Touch on Mac 문서: 마이그레이션 가이드, 게임 기술 노트, HIG
Touch on Mac에 대해 Apple은 Tech Talk 이후 무엇을 공개했습니까? 10월 28일 저녁까지, 정오에는 404를 반환하던 AppKit 아티클 “Adopting gesture recognizers in your app”이 공개되었고, 여기에는 모든 종류의 마우스 처리 코드에 다음 단계를 제시하는 4행짜리 표가 있습니다. 두 번째 아티클 “Handling AppKit gestures”는 배타적 뷰, 멈춘 인식기에 대한 2초 타임아웃, 디버깅용 기본값 두 개를 설명합니다. 새 기술 노트 TN3216은 게임과 에뮬레이션된 마우스 이벤트의 순서를 다루고, Human Interface Guidelines 페이지 3개에는 Mac의 터치에 관한 10월 28일자 변경 이력 항목이 붙었습니다.1234 랜딩 페이지의 “Documentation resources” 타일은 이제 아티클과 파일 프로미스 샘플을, “Design resources” 타일은 HIG 페이지 3개를 링크하며, Xcode 27.3 베타 타일은 여전히 “Coming soon”입니다.5
요약: 10월 28일 오후 1시 47분(PT)에 공개한 개발자 글은 Tech Talk 3개와 6월의 기술 노트를 바탕으로 썼고, 그 글에 필요했던 AppKit 아티클은 정오에 404를 반환했습니다. 오후 9시 51분에는 그 아티클이 공개되어 있었고, 옆에 문서가 두 개 더 있었습니다. 마이그레이션 아티클은 코드를 네 행으로 분류합니다. 표준 컨트롤과 인식기는 할 일이 없습니다. mouseDown(with:) 오버라이드와 트래킹 루프는 한 손가락 에뮬레이션을 받으며 인식기로 바꿔야 합니다. 마우스 메서드를 오버라이드한 컨트롤 서브클래스는 트래킹 루프로 되돌아갑니다. 끝나지 않는 커스텀 인식기는 자신의 배타적 뷰를 붙잡고 있습니다.1 세 번째 행의 규칙은 AppKit이 메서드를 오버라이드했는지 여부로 판단하며 “not on what the override does”(오버라이드가 무엇을 하는지로 판단하지 않는다)라는 것이므로, super를 호출해도 소용이 없습니다.1 유지하는 트래킹 루프 옆에 buttonMask를 0으로 둔 터치 전용 NSPanGestureRecognizer를 두는 방법이 이제 문서화되었고, 마스크를 그대로 두었을 때의 결과도 정확히 적혀 있습니다. 마우스 드래그가 인식기를 시작시키고 mouseDown(with:)은 실행되지 않습니다.1 배타적 뷰는 Info.plist 키가 없으면 기본으로 켜집니다. Xcode의 macOS App 템플릿은 앱 라이프사이클이 AppKit 앱 델리게이트인 프로젝트에서만 이 키를 NO로 쓰고, 기본값인 SwiftUI 라이프사이클에서는 쓰지 않습니다. 이 Mac에 있는 Xcode 27.1과 27.2 베타의 템플릿에서 읽은 내용입니다.27 멈춘 인식기는 상호작용이 끝나고 2초 뒤에 취소되며, 디버깅하는 동안에는 사용자 기본값 하나가 오버라이드 누락 시 크래시를 일으키고 다른 하나가 조용한 취소를 크래시로 바꿉니다.2 게임 기술 노트는 탭과 드래그가 만들어내는 마우스 이벤트를 정확히 나열하고, 엔진에 델타를 누적하지 말라고 하며, NSIsTouchNative를 설정하기 전까지 각 터치 표면이 추가로 에뮬레이션된 GCMouse로 나타난다고 설명합니다.3 HIG는 터치를 “a secondary input mode”(보조 입력 모드)라고 부르고, “tap targets bigger, not the controls themselves”(컨트롤 자체가 아니라 탭 타깃을 크게)를 말하며, 호버에서만 나타나던 컨트롤에 관한 규칙 6개를 더합니다.4 아직 없는 것은 Xcode 27.3 베타와 시스템 제스처 스위치 3개이며, 오후 9시 51분(PT)에도 404를 반환합니다.56
무엇이, 언제 올라왔는가
아래 항목에는 각각 제가 확인한 시각을 붙였습니다. 서버 자체의 스탬프가 하루 동안 페이지에 실제로 보인 내용과 맞지 않기 때문입니다. 이날의 기록은 라이브 포스트에 있으며, 이번 공개를 다룬 늦은 행도 거기에 있습니다.
- AppKit 아티클. “Adopting gesture recognizers in your app”은 정오(PT)에 Apple 문서 엔드포인트에서 404를 반환했습니다. 오후 9시 51분에는 공개되어 있었고, 함께 온 수정 스탬프는 오후 2시 30분(PT)이었으며, AppKit의 Essentials 섹션에서 “Adopting Liquid Glass” 다음에, 그리고 프레임워크의 Gestures 페이지에서 “Gesture handling” 아래에 올라 있었습니다. Wayback Machine의 첫 캡처는 오후 6시 8분(PT)입니다(근거는 출처에 있습니다).1 “Bring Touch to your Mac app” Tech Talk 페이지는 여전히 이 아티클을 링크하지 않습니다.5
- 두 번째 아티클. “Handling AppKit gestures”는 Gestures 페이지에서 그 옆에 있습니다. 개정 이력이 없고, 6월의 기술 노트는 이를 링크하지 않으며, Wayback Machine에는 캡처가 없고, 서버 스탬프는 10월 27일 오후 10시 35분(PT)입니다. 정오에 확인한 페이지에 포함되지 않았으므로, 언제 올라왔는지는 말할 수 없습니다.2
- 새 기술 노트. “TN3216: Supporting touch input for games on macOS”의 개정 이력은 “2026-10-28 First published” 한 줄이고, 서버 스탬프는 오후 2시 9분(PT)입니다.3
- HIG 페이지 3개. “Designing for macOS”, “Gestures”, “Pointing devices”는 이제 2026년 10월 28일자 변경 이력 행으로 끝납니다. 정오에 가져온 사본에는 새 텍스트가 전혀 없었고, 오후 9시 51분에 가져온 사본에는 있습니다. 새 “Gestures” 페이지의 서버 수정 헤더는 10월 27일 5시 11분(UTC)으로, 새 텍스트가 없던 정오 사본보다 이릅니다. 두 사실을 모두 적되, 공개 시각은 적지 않습니다.4
- 랜딩 페이지. 오전 11시 49분에는 “Tools and documentation” 아래 타일 3개가 모두 “Coming soon”이었고, 링크는 HTML 주석 안에 보관되어 있었습니다. 오후 9시 51분에는 “Documentation resources” 타일(“Learn how to build and optimize your app for touch input.”)이 아티클과 파일 프로미스 샘플을 링크하고, “Design resources” 타일(“Create experiences that take advantage of touch user interface patterns.”)이 HIG 페이지 3개와 Apple Design Resources를 링크합니다. Apple Design Resources의 제품 베젤 목록에는 “MacBook Pro PROLED XDR Display”가 있습니다. “Xcode 27.3 beta” 타일은 여전히 “Coming soon”이고, HTML 주석
TK skill도 페이지에 그대로 있습니다.5 - 샘플. “Supporting drag and drop through file promises” 페이지는 이제 인식기에 대해 “The recognizers work the same way with a pointer or with direct touches on a Mac with a touch display.”(인식기는 포인터로도, 터치 디스플레이 Mac에서의 직접 터치로도 똑같이 동작한다)라고 말합니다.6
- 그 밖에는 움직이지 않았습니다. 개발자 뉴스 피드의 최신 항목은 여전히 오전 10시 00분 55초(PDT)의 “Say hello to Touch on Mac”입니다. 릴리스 피드의 최신 항목은 10월 26일의 포인트 릴리스들이며, Xcode 27.3이나 macOS 27.3 베타는 없습니다. Tech Talks 목록에는 같은 Tech Talk 3개가 있고, ActivityKit 페이지는 10월 8일 스탬프를 달고 있으며 여전히 macOS 플랫폼을 나열하지 않습니다.56
오후 글은 문서화된 규칙이 Tech Talk들과 6월의 기술 노트라고 말했습니다. 이 글은 문서화된 규칙이 무엇을 더하는지에 관한 것이며, 저녁 확인에서 다른 정보가 나온 부분은 오후 글을 바로잡습니다. 그 목록은 끝에 있습니다.
코드를 분류하는 표
아티클의 “Prepare your app for a Mac with a touch display” 섹션은 입력 처리, 터치에서의 동작, 다음 단계라는 세 열과 네 행으로 된 표입니다. Apple의 동작 열은 인용했고, 나머지 두 열은 요약했습니다. 단, 세 번째 행의 다음 단계는 Apple의 문구입니다.1
| 코드 | Apple의 “Behavior with touch” | 다음 단계 |
|---|---|---|
| 표준 AppKit 컨트롤과 제스처 인식기 | “Works with touch, with no changes needed.”(변경 없이 터치로 동작한다) | 테스트합니다. |
mouseDown(with:) 오버라이드와 트래킹 루프 |
“AppKit emulates a single-finger tap or drag, but supports multi-finger gestures and individual touches only through gesture recognizers.”(AppKit은 한 손가락 탭이나 드래그를 에뮬레이션하지만, 여러 손가락 제스처와 개별 터치는 제스처 인식기로만 지원한다) | 인식기로 교체합니다. |
마우스 처리 메서드를 오버라이드한 NSButton, NSSlider 및 그 밖의 AppKit 뷰의 서브클래스 |
“AppKit falls back to a tracking loop.”(AppKit이 트래킹 루프로 되돌아간다) | “Replace the overrides with target-action pairs for tracking events.”(오버라이드를 트래킹 이벤트용 타깃-액션 쌍으로 교체하라) |
커스텀 NSGestureRecognizer 서브클래스 |
“A recognizer that never reaches a terminal state keeps its exclusive view, which can make your app appear unresponsive.”(종료 상태에 도달하지 않는 인식기는 배타적 뷰를 계속 붙잡아, 앱이 응답하지 않는 것처럼 보이게 할 수 있다) | 취소를 처리하고 종료 상태에 도달하게 합니다. |
표 다음의 테스트는 네 줄이며, 쓰인 그대로 실행해볼 만합니다. “Controls respond to a tap, and buttons highlight and activate as expected”, “Dragging a view, selecting text, and scrolling work with one finger”, “Pinch and rotate work where your app supports them”, 그리고 “Small targets, such as resize grips and grab handles, are easy to hit with a finger.”입니다(컨트롤이 탭에 반응하고 버튼이 예상대로 강조되고 활성화되는가, 뷰 드래그와 텍스트 선택과 스크롤이 한 손가락으로 되는가, 앱이 지원하는 곳에서 핀치와 회전이 되는가, 크기 조절 그립이나 잡기 핸들 같은 작은 타깃을 손가락으로 쉽게 맞힐 수 있는가).1 검색 목록은 Tech Talk의 mouseDown(with:) 오버라이드 점검을 심볼 5개로 넓힙니다. “search your project for nextEvent(matching:until:inMode:dequeue:) and trackEvents(matching:timeout:mode:handler:), because tracking loops typically call these methods. Also search for mouseDown(with:) and mouseDragged(with:) overrides, and for uses of currentEvent in control action methods.”(트래킹 루프가 보통 이 메서드들을 호출하므로 프로젝트에서 nextEvent(matching:until:inMode:dequeue:)와 trackEvents(matching:timeout:mode:handler:)를 검색하라. mouseDown(with:)과 mouseDragged(with:) 오버라이드, 그리고 컨트롤 액션 메서드에서 currentEvent를 쓰는 곳도 검색하라)1 마지막 점검은 네 번째 행이 다루는 것입니다. “confirm that your app stays responsive after a touch interaction ends, and that every gesture recognizer action method handles the NSGestureRecognizer.State.cancelled state.”(터치 상호작용이 끝난 뒤에도 앱이 응답하는지, 그리고 모든 제스처 인식기 액션 메서드가 NSGestureRecognizer.State.cancelled 상태를 처리하는지 확인하라)1
Overview의 두 문장은 Tech Talk가 추론에 맡겨두었던 질문을 정리합니다. 에뮬레이션은 한 손가락입니다. “AppKit emulates mouse events from a single finger tapping or dragging”(AppKit은 한 손가락의 탭이나 드래그로부터 마우스 이벤트를 에뮬레이션한다), 그리고 “Relying on this emulation, however, means you miss out on multi-finger gestures, like pinch and rotate, that gesture recognizers handle.”(그러나 이 에뮬레이션에 의존하면 제스처 인식기가 처리하는 핀치와 회전 같은 여러 손가락 제스처를 놓치게 된다)1 그리고 테스트 장비는 Tech Talk가 말한 그대로이며, 이제 팁 상자에 들어 있습니다. “Use Sidecar to test touch input without a Mac that has a touch display”(터치 디스플레이가 있는 Mac 없이 Sidecar로 터치 입력을 테스트하라), 이유는 “AppKit then routes touch on its screen the same way it does on a Mac with a touch display.”(그러면 AppKit이 그 화면의 터치를 터치 디스플레이 Mac에서와 같은 방식으로 전달한다)입니다.1
오버라이드 규칙
표의 세 번째 행 뒤에는 Tech Talk도 6월의 기술 노트도 이만큼 분명히 말하지 않았던 규칙이 있습니다. NSButton을 서브클래싱하고 강조 애니메이션을 더하려고 mouseDown(with:)을 오버라이드하면, “AppKit treats your override as a replacement for its own click handling, rather than as a side effect”(AppKit은 오버라이드를 부수 효과가 아니라 자체 클릭 처리의 대체물로 취급한다)이며, 버튼은 트래킹 루프로 되돌아갑니다.1 이어서 “AppKit decides based on whether your subclass overrides the method, not on what the override does”(AppKit은 오버라이드가 무엇을 하는지가 아니라 서브클래스가 메서드를 오버라이드했는지로 판단한다)이므로, 오버라이드가 super.mouseDown(with:)을 호출해도 이 폴백이 적용되고, “Overriding mouseDragged(with:) or mouseUp(with:) has the same effect.”(mouseDragged(with:)나 mouseUp(with:)을 오버라이드해도 결과는 같다)입니다.1
대체 방법은 Tech Talk에 나온 컨트롤 이벤트 패턴이며, 아티클의 코드 목록은 Tech Talk의 것과 한 가지 세부에서 다르고, 그 세부는 그대로 따라 할 만합니다. Tech Talk의 2:24 코드 목록은 [.trackingExited, .trackingEndedInside]에 액션 하나를, .trackingCancelled에 별도의 액션을 등록했습니다. 아티클의 숨김 집합은 [.trackingExited, .trackingEndedInside, .trackingEndedOutside, .trackingCancelled]이며, 버튼 밖에서 끝나는 누르기가 추가됩니다.19 trackingEndedOutside는 새로운 것이 아닙니다. Apple 문서는 이를 macOS 11.0으로 올려두었고, macOS 27.0 SDK는 NSControl.h에서 이를 NSControlEventTrackingEndedOutside = 1 << 7로 선언합니다(필자 분석).67 아티클은 또한 액션 메서드를 @objc로 Objective-C에 노출해야 한다는 점, 그리고 활성화만 필요하다면 primaryActionTriggered가 “regardless of whether someone used a mouse, trackpad, or touch to produce it”(마우스, 트랙패드, 터치 중 무엇으로 일으켰든 상관없이) 발생한다는 점을 적어둡니다.1
컨테이너 뷰에도 같은 함정이 있습니다. “If you start a drag from mouseDragged(with:) in an NSTableView, NSCollectionView, NSOutlineView, or NSBrowser, AppKit falls back to a tracking loop for the interaction”(NSTableView, NSCollectionView, NSOutlineView, NSBrowser에서 mouseDragged(with:)로 드래그를 시작하면 AppKit은 그 상호작용에 트래킹 루프로 되돌아간다)이며, 빠져나가는 길은 데이터 소스와 델리게이트의 드래그 메서드입니다. 테이블이라면 tableView(_:pasteboardWriterForRow:)입니다.1
흔한 mouseDown(with:) 오버라이드 두 가지에 대해 아티클은 변경 전후 코드 쌍을 제시합니다. “Most mouseDown(with:) overrides do one of two things: mark an item as selected when someone clicks it, or show a menu when someone Control-clicks.”(대부분의 mouseDown(with:) 오버라이드는 둘 중 하나를 한다. 클릭하면 항목을 선택된 것으로 표시하거나, Control-클릭하면 메뉴를 보여준다)1 선택의 경우에는 대신 컬렉션 뷰 항목이나 테이블 행 뷰의 isSelected를 관찰하되, 관찰자가 “also runs when your code changes the selection programmatically”(코드가 프로그래밍 방식으로 선택을 바꿀 때도 실행된다)라는 단서가 붙습니다. 메뉴의 경우에는 menu(for:)를 오버라이드해 AppKit이 메뉴를 표시하게 하거나, 메뉴가 클릭에 좌우되지 않는다면 menu나 defaultMenu를 설정합니다.1
유지하는 루프 옆의 터치 전용 인식기
Tech Talk는 기존 트래킹 루프 옆에 터치 전용 팬 인식기를 두는 것을 마이너 릴리스용 경로로 권했습니다. 아티클은 같은 방법을 “Add touch support to your custom tracking loop” 아래에, 공유하는 시작, 계속, 종료, 취소 함수와 함께 문서화하고, 이어서 각 속성이 하는 일을 Tech Talk가 다룰 여유가 없었던 정밀함으로 설명합니다.1
let touchRecognizer = NSPanGestureRecognizer(target: self, action: #selector(handleTouchPan))
touchRecognizer.buttonMask = 0
touchRecognizer.allowedTouchTypes = .direct
addGestureRecognizer(touchRecognizer)
“setting buttonMask to 0 means the gesture recognizer only recognizes touches, not mouse or trackpad input, so your tracking loop keeps handling the mouse and trackpad exactly as before.”(buttonMask를 0으로 설정하면 제스처 인식기는 마우스나 트랙패드 입력이 아니라 터치만 인식하므로, 트래킹 루프는 마우스와 트랙패드를 이전과 똑같이 계속 처리한다)1 마스크를 기본값으로 두면 인식기는 “also responds to the primary mouse button. A mouse drag then begins the gesture recognizer, and AppKit doesn’t call your mouseDown(with:) override, so the tracking loop doesn’t run. A click that doesn’t move still reaches your tracking loop.”(기본 마우스 버튼에도 반응한다. 그러면 마우스 드래그가 제스처 인식기를 시작시키고, AppKit은 mouseDown(with:) 오버라이드를 호출하지 않으므로 트래킹 루프가 실행되지 않는다. 움직이지 않는 클릭은 여전히 트래킹 루프에 도달한다)1 그러니 마스크는 있으면 좋은 정도의 것이 아닙니다. 마스크가 없으면 루프는 아무 표시 없이 드래그를 받지 못하게 됩니다. allowedTouchTypes 줄은 이중 안전장치입니다. “Direct touches are already the default for allowedTouchTypes, and setting it to .direct states that intent explicitly.”(직접 터치는 이미 allowedTouchTypes의 기본값이며, .direct로 설정하면 그 의도를 명시적으로 밝히게 된다)1 이 접근에 대한 Apple 자신의 평가는 이렇습니다. “This is often the fastest, lowest-risk way to add touch support to code with a tracking loop you’d rather not rewrite all at once.”(한꺼번에 다시 쓰고 싶지 않은 트래킹 루프가 있는 코드에 터치 지원을 더하는 가장 빠르고 위험이 적은 방법인 경우가 많다)1
대신 루프를 교체한다면, 아티클의 팬 예제는 .cancelled에서 캡션을 시작 위치로 되돌리고, 드래그 세션을 시작하던 루프는 이벤트 대신 인식기를 beginDraggingSession(items:gesture:source:)에 넘깁니다.1
델리게이트 메서드 두 개로 거르기
Tech Talk들에 비해 가장 새로운 섹션은 “Filter gesture recognizer candidates”입니다. 인식기는 리스폰더 체인이 아니라 히트 테스트로 수집됩니다. “Gesture recognizers don’t use the responder chain.”(제스처 인식기는 리스폰더 체인을 쓰지 않는다)1 AppKit이 인식기를 수집할 때 “not every recognizer it finds is actually applicable to the interaction”(찾아낸 인식기가 모두 그 상호작용에 실제로 해당하지는 않는다)이며, 앱은 델리게이트 메서드 두 개로 해당하지 않는 것을 걸러냅니다. gestureRecognizer(_:shouldAttemptToRecognizeWith:)는 “to inspect the initiating NSEvent for pointer-based input”(포인터 기반 입력에서 시작 NSEvent를 검사하기 위해), gestureRecognizer(_:shouldReceive:)는 “to inspect each new NSTouch for touch input”(터치 입력에서 새 NSTouch 각각을 검사하기 위해) 씁니다.1 두 메서드의 요점은 이렇습니다. “Either method gives you the initial input, something the recognizer doesn’t otherwise expose, and lets you stop the recognizer from handling that event stream or touch.”(어느 메서드든 인식기가 달리 노출하지 않는 최초 입력을 넘겨주고, 인식기가 그 이벤트 스트림이나 터치를 처리하지 못하게 막을 수 있게 한다)1 그리고 구분은 엄격합니다. “Direct touches don’t call this method. To filter them, implement gestureRecognizer(:shouldReceive:) instead, as shown above.”(직접 터치는 이 메서드를 호출하지 않는다. 직접 터치를 거르려면 위에서 보인 것처럼 대신 gestureRecognizer(:shouldReceive:)를 구현하라)1 이벤트 메서드로만 거르면, 직접 터치는 그 델리게이트에서 걸러지지 않은 채로 남습니다.
두 예제는 그대로 복사되는 것을 보게 될 것 같은 패턴입니다. 크기 조절 그립의 경우, 크기 조절 인식기를 뷰 전체에 붙이고 두 델리게이트 메서드 모두에서 위치가 그립의 프레임 안에 있는지를 반환하되, true는 “only to give the gesture recognizer a chance to attempt recognition, not to guarantee it succeeds”(성공을 보장하려는 것이 아니라 제스처 인식기에 인식을 시도할 기회를 주려는 것뿐)입니다.1 클립 타임라인의 경우, 타임라인에 인식기 하나를 두고 이벤트 메서드에서 클립을 히트 테스트해 액션 메서드용으로 저장하며, 클립이 없으면 false를 반환합니다.1 작은 타깃에 관한 짝이 되는 경고는 “If a custom control expects a touch to land inside its visible area, update it to handle touches that land outside that area.”(커스텀 컨트롤이 터치가 보이는 영역 안에 떨어진다고 가정한다면, 그 영역 밖에 떨어지는 터치도 처리하도록 고쳐라)입니다.1
걸러지고 남은 인식기 사이의 우선순위는 실패 요구 조건으로 정합니다. 아티클은 두 번째 아티클의 경고 “Use this sparingly, because the waiting recognizer responds more slowly”(기다리는 인식기는 더 느리게 반응하므로 아껴 써라)를 되풀이하고, “making a single click wait for a double click to fail makes clicks feel sluggish”(싱글 클릭이 더블 클릭의 실패를 기다리게 하면 클릭이 굼뜨게 느껴진다)라는 예를 듭니다.1
currentEvent, 호버, 스와이프
“Handle touch input”은 Tech Talk의 핵심 문장을 문서 형태로 다시 말합니다. AppKit은 “doesn’t deliver direct touches (NSTouch) through the responder chain or through event monitors”(직접 터치(NSTouch)를 리스폰더 체인이나 이벤트 모니터로 전달하지 않는다)이며, “Gesture recognizers are the only way to respond to multi-finger gestures or to receive individual touches from a Mac with a touch display or a Sidecar-connected iPad.”(여러 손가락 제스처에 반응하거나, 터치 디스플레이 Mac 또는 Sidecar로 연결한 iPad에서 개별 터치를 받는 유일한 방법은 제스처 인식기다)1 AppKit 입력 개요 페이지는 리스폰더 쪽에서 같은 말을 합니다. “The responder chain receives touch events only from indirect touches on a trackpad, if a view asks for them. It doesn’t receive touch events from direct touches on a Mac with a touch display.”(리스폰더 체인은 뷰가 요청하는 경우 트랙패드의 간접 터치에서만 터치 이벤트를 받는다. 터치 디스플레이 Mac의 직접 터치에서는 터치 이벤트를 받지 않는다)6
기능 확인에 관한 조언은 하지 말라는 것입니다. “In most cases, instead of checking whether a device supports touch, write your event-handling code to work the same way regardless of input source, and decide on alternate presentations based on the interaction type.”(대부분의 경우 기기가 터치를 지원하는지 확인하는 대신, 입력 소스와 상관없이 똑같이 동작하도록 이벤트 처리 코드를 쓰고, 상호작용 유형에 따라 다른 표현을 정하라)1 touchCapabilities에 대해 든 유일한 예는 “capturing a signature on a touch display”(터치 디스플레이에서 서명 받기)입니다.1
currentEvent에는 섹션 하나가 따로 있습니다. 컨트롤 상호작용이 인식기를 거치기 때문에 “currentEvent no longer reliably returns the event that initiated an action”(currentEvent는 더 이상 액션을 시작한 이벤트를 확실하게 반환하지 않는다)이며, 변경 전후 코드 쌍은 이벤트의 수정자 플래그를 클래스 수준의 NSEvent.modifierFlags로 바꾸면서 “These report the state at the time you query them, not at the time of the original event. A touch doesn’t register in pressedMouseButtons.”(이 값들은 원래 이벤트 시점이 아니라 조회한 시점의 상태를 보고한다. 터치는 pressedMouseButtons에 기록되지 않는다)라는 단서를 붙입니다.1 바뀌지 않는 것은 이렇습니다. “Keyboard events and scroll-wheel events behave the same on every input source: AppKit delivers them through the responder chain. Hover tracking, which relies on mouseMoved(with:) and NSTrackingArea, applies to pointer input only, because a touch display has no hover.”(키보드 이벤트와 스크롤 휠 이벤트는 모든 입력 소스에서 똑같이 동작한다. AppKit이 리스폰더 체인으로 전달한다. mouseMoved(with:)와 NSTrackingArea에 의존하는 호버 추적은 포인터 입력에만 적용된다. 터치 디스플레이에는 호버가 없기 때문이다)1
스와이프는 변환되지 않습니다. “AppKit doesn’t translate touch swipes for you.”(AppKit은 터치 스와이프를 대신 변환해주지 않는다)1 trackSwipeEvent(options:dampenAmountThresholdMin:max:usingHandler:)를 쓴다면 직접 구현해야 하고, 뒤로 가기와 앞으로 가기 탐색에서는 먼저 NSEvent.isTouchSwipeNavigationEnabled를 확인하십시오. “because the person can turn off swipe navigation in System Settings”(사용자가 시스템 설정에서 스와이프 탐색을 끌 수 있기 때문)입니다.1 이 속성은 새것입니다. Apple 문서는 이를 macOS 27.0으로 올려두었고, macOS 27.0 SDK의 NSEvent.h는 496행에서 이를 API_AVAILABLE(macos(27))로 선언합니다(필자 분석).67
커스텀 인식기: 계약, 타임아웃, 기본값 두 개
커스텀 NSGestureRecognizer 서브클래스에 대한 아티클의 지침은 글머리표 7개로 된 계약입니다. mouseCancelled(with:)와 touchesCancelled(with:)에서 취소를 처리할 것, 이벤트 메서드에서는 비공개 트래킹 상태를 갱신하고 state를 설정하는 일만 할 것, 이벤트 콜백이 아니라 액션 메서드에서 반응할 것, 액션 메서드를 위해 시작 NSEvent나 NSTouch를 캐시할 것, “Override reset() to clear your cached state, so it doesn’t leak from one gesture into the next”(캐시한 상태가 한 제스처에서 다음 제스처로 새지 않도록 reset()을 오버라이드해 지우라), “Always end an active gesture recognizer by transitioning to NSGestureRecognizer.State.ended, NSGestureRecognizer.State.failed, or NSGestureRecognizer.State.cancelled. Otherwise, other recognizers can’t activate”(활성 제스처 인식기는 항상 NSGestureRecognizer.State.ended, NSGestureRecognizer.State.failed, NSGestureRecognizer.State.cancelled 중 하나로 전환해 끝내라. 그러지 않으면 다른 인식기가 활성화되지 못한다), 그리고 location(in:)을 오버라이드할 것입니다. 마지막 항목의 이유는 “If you skip this override and build your app with the macOS 27 SDK and later, AppKit returns NSZeroPoint and logs a console error.”(이 오버라이드를 빼고 macOS 27 SDK 이상으로 앱을 빌드하면 AppKit은 NSZeroPoint를 반환하고 콘솔 오류를 기록한다)입니다.1 뒤따르는 DragTrackingRecognizer 예제는 뼈대입니다. 마우스 다운, 드래그, 업 오버라이드의 본문은 비공개 메서드 track(_:event:state:) 하나를 호출하라는 주석이고, 취소 오버라이드의 본문은 .cancelled를 설정하라는 주석이며, 액션 메서드는 캐시한 위치와 이벤트를 다시 읽습니다.1
계약을 어겼을 때의 결과가 수치로 나오는 곳은 두 번째 아티클입니다. “A gesture recognizer is stuck if it hasn’t reached a terminal state within 2 seconds after the person stops interacting with your app, such as when they release the mouse button or lift their fingers.”(사용자가 마우스 버튼을 놓거나 손가락을 떼는 등 앱과의 상호작용을 멈춘 뒤 2초 안에 종료 상태에 도달하지 않으면, 그 제스처 인식기는 멈춘 것이다)2 배타적 뷰 아래에서는 “A stuck gesture recognizer keeps holding its exclusive view”(멈춘 제스처 인식기는 배타적 뷰를 계속 붙잡는다)이며, 이 때문에 그 계층 밖의 수집이 막힙니다. 시스템의 해결책은 자동입니다. “To keep a single stuck gesture recognizer from blocking your app indefinitely, the system automatically transitions it to the NSGestureRecognizer.State.cancelled state, allowing gesture gathering to resume normally.”(멈춘 제스처 인식기 하나가 앱을 무한정 막지 않도록, 시스템은 이를 자동으로 NSGestureRecognizer.State.cancelled 상태로 전환해 제스처 수집이 정상적으로 재개되게 한다)2 이것이 표 네 번째 행 뒤의 메커니즘입니다. 즉 배포 중인 앱에서 고장 난 인식기는, 사용자가 상호작용을 멈추고 2초 뒤에 자동 취소가 일어날 때까지 배타적 계층 밖의 제스처 수집을 막아 앱의 나머지 부분이 응답하지 않는 것처럼 보이게 하며, 그 취소를 처리할 책임은 여전히 액션의 타깃에 있습니다.
사용자 기본값 두 개는 개발하는 동안 이런 문제를 스택 트레이스와 함께 크래시로 만듭니다. 하나는 오버라이드 누락에 대해, 다른 하나는 조용한 취소 대신 작동합니다. “Set the NSGestureRecognizerCrashOnMissingOverrides key in UserDefaults to YES”(UserDefaults에서 NSGestureRecognizerCrashOnMissingOverrides 키를 YES로 설정하라)를 따르거나 실행 인수로 -NSGestureRecognizerCrashOnMissingOverrides YES를 넘기면, 오버라이드했어야 할 이벤트 메서드가 빠졌을 때 크래시가 납니다. 그리고 NSCrashOnStuckGestureTimeout을 “to a small number of seconds, such as 2”(2처럼 작은 초 단위 값으로) 설정하거나 -NSCrashOnStuckGestureTimeout 2를 넘기면, 멈춘 인식기를 취소하는 대신 크래시가 납니다.2 첫 번째 기본값이 모든 것을 잡지는 못합니다. 이 값은 “doesn’t detect a gesture recognizer that correctly transitions to NSGestureRecognizer.State.cancelled and sends its action, if the action’s target doesn’t handle the canceled state.”(NSGestureRecognizer.State.cancelled로 올바르게 전환하고 액션을 보내는 제스처 인식기라도, 액션의 타깃이 취소 상태를 처리하지 않는 경우는 감지하지 못한다)2 이어지는 경고는 체크리스트에 들어가야 할 종류입니다. “Comment out or remove NSGestureRecognizerCrashOnMissingOverrides and NSCrashOnStuckGestureTimeout before shipping your app”(앱을 배포하기 전에 NSGestureRecognizerCrashOnMissingOverrides와 NSCrashOnStuckGestureTimeout을 주석 처리하거나 제거하라), 그리고 “Changes to these user defaults or launch arguments take effect only after you rebuild or relaunch your app.”(이 사용자 기본값이나 실행 인수의 변경은 앱을 다시 빌드하거나 다시 실행한 뒤에야 적용된다)2 두 기본값 모두 Tech Talk에도 6월의 기술 노트에도 나오지 않습니다.
같은 아티클은 성공이 아닌 두 종료 상태의 차이를 설명합니다. .failed는 한 번도 일치하지 않은 인식기를 위한 것이고, “If a gesture recognizer sets this after it already delivered an action, the system automatically converts it to NSGestureRecognizer.State.cancelled instead.”(제스처 인식기가 이미 액션을 전달한 뒤에 이 상태를 설정하면, 시스템이 자동으로 NSGestureRecognizer.State.cancelled로 바꾼다)2 또한 Tech Talk의 Exposé 예 말고도 시스템 취소의 원인을 하나 더 듭니다. “when the same touches begin a system-level gesture like smartMagnify(with:), the system takes over those touches for its own use.”(같은 터치가 smartMagnify(with:) 같은 시스템 수준 제스처를 시작하면, 시스템이 그 터치를 가져가 직접 쓴다)2
같은 페이지의 작은 사실 세 가지도 있습니다. 인식기는 리스폰더 이벤트를 지연시킬 수 있습니다. “If you set delaysPrimaryMouseButtonEvents to true, the system stores the relevant events until the gesture recognizer fails, then delivers them to the appropriate responders. If the gesture recognizer reaches the NSGestureRecognizer.State.ended state, the system never delivers the events.”(delaysPrimaryMouseButtonEvents를 true로 설정하면 시스템은 제스처 인식기가 실패할 때까지 관련 이벤트를 보관했다가 적절한 리스폰더에 전달한다. 제스처 인식기가 NSGestureRecognizer.State.ended 상태에 도달하면 시스템은 그 이벤트를 전달하지 않는다)2 그리고 리스폰더 자체의 터치 메서드는 트랙패드 전용입니다. “These methods don’t have an equivalent delay mechanism, and they apply only to indirect touches from a trackpad.”(이 메서드들에는 그에 해당하는 지연 메커니즘이 없고, 트랙패드의 간접 터치에만 적용된다)2 뷰 컨트롤러는 인식기를 가질 수 없습니다. “You can’t attach a gesture recognizer to a view controller directly”(제스처 인식기를 뷰 컨트롤러에 직접 붙일 수 없다)이므로, 컨트롤러의 뷰에 붙이고 컨트롤러를 타깃이자 델리게이트로 지정하십시오.2
배타적 뷰: 기본은 켜짐, Xcode의 AppKit 템플릿에서는 꺼짐
오후 글은 Tech Talk와 6월의 기술 노트로 배타성을 설명했습니다. 두 아티클은 여기에 기본값, 메커니즘, 그리고 Xcode에 관한 사실 하나를 더합니다.
메커니즘은 macOS 27에서 새로 생겼습니다. “In macOS 27 and later, the initial hit-tested view for a mouse click or touch event becomes an exclusive view for gathering gesture recognizers”(macOS 27 이상에서는 마우스 클릭이나 터치 이벤트에 대해 처음 히트 테스트된 뷰가 제스처 인식기 수집을 위한 배타적 뷰가 된다), 그리고 “This behavior simulates the presence of a single mouse pointer, even when a person touches a Mac with a touch display with several fingers at once.”(이 동작은 사용자가 터치 디스플레이 Mac을 여러 손가락으로 동시에 터치하더라도 마우스 포인터 하나만 있는 상황을 흉내 낸다)2 기본값은 이렇습니다. “Exclusive views are on by default”(배타적 뷰는 기본으로 켜져 있다), 그리고 “If your app’s information property list doesn’t include the NSViewGestureRecognizerIsExclusive key, the system treats it as YES.”(앱의 정보 속성 목록에 NSViewGestureRecognizerIsExclusive 키가 없으면 시스템은 이를 YES로 취급한다)2 키 자체의 페이지도 “The default value is YES”(기본값은 YES다)라고 같은 말을 하며, 해제를 좁게 규정합니다. “Set this key to NO to allow gesture recognizers in multiple views to activate simultaneously. Do this only if your app intentionally needs concurrent gesture recognition across different views.”(여러 뷰의 제스처 인식기가 동시에 활성화되게 하려면 이 키를 NO로 설정하라. 앱이 의도적으로 서로 다른 뷰에 걸친 동시 제스처 인식을 필요로 할 때만 그렇게 하라)6 뷰 단위 해제에 대해 시스템이 드는 예는 색상 패널입니다. “AppKit uses this same technique in the standard Color Panel”(AppKit은 표준 색상 패널에서 이와 같은 기법을 쓴다)이며, 거기서는 여러 슬라이더가 동시에 움직입니다.2
Xcode에 관한 사실은 두 아티클 모두에 적혀 있습니다. “Xcode adds the NSViewGestureRecognizerIsExclusive key with a value of NO to new macOS app projects, so those apps don’t use exclusive views unless you change the value to YES.”(Xcode는 새 macOS 앱 프로젝트에 값이 NO인 NSViewGestureRecognizerIsExclusive 키를 추가하므로, 값을 YES로 바꾸지 않는 한 그 앱들은 배타적 뷰를 쓰지 않는다)1 이것이 어떤 프로젝트에 해당하는지 보려고 이 Mac의 프로젝트 템플릿을 읽었습니다(필자 분석). Xcode 27.1(27A9275)과 Xcode 27.2 베타(27B5019j)에서 macOS App 템플릿은 “Cocoa App Base” 유닛을 상속하며, 이 유닛의 앱 라이프사이클 옵션에는 SwiftUI와 Cocoa라는 두 값이 있고 기본값은 SwiftUI입니다. 값이 NO인 키 INFOPLIST_KEY_NSViewGestureRecognizerIsExclusive는 Cocoa 유닛 안에만 있습니다. 따라서 Xcode는 프로젝트가 AppKit 앱 델리게이트 라이프사이클을 쓸 때 이 키를, 생성된 Info.plist에 들어가는 빌드 설정으로 쓰고, SwiftUI 라이프사이클을 쓸 때는 쓰지 않습니다.7 라이프사이클 옵션에는 템플릿 안에서 표시 이름이 없습니다. 표시 이름이 있는 옵션은 Interface:(SwiftUI, Storyboard, XIB, 기본값 SwiftUI)이며, Storyboard와 XIB 유닛은 AppKit 라이프사이클을 요구합니다. 그래서 Storyboard나 XIB 프로젝트는 이 키를 받고, SwiftUI 인터페이스 프로젝트는 라이프사이클이 AppKit 앱 델리게이트일 때만 받습니다.7 Document App 템플릿은 같은 베이스에서 내려오므로, 같은 조건에서 그 빌드 설정을 상속하고 이 키를 값이 false인 Info.plist 항목으로도 씁니다. macOS Game 템플릿은 Cocoa 라이프사이클을 강제하므로, 새 macOS 게임은 비배타적으로 시작합니다.7 이 Mac에는 27.1보다 오래된 Xcode가 없으므로, 템플릿이 언제 이 키를 갖게 되었는지는 말할 수 없습니다. 실질적으로 읽으면 이렇습니다. 이 템플릿으로 만든 AppKit 프로젝트는 비배타적으로 시작하고, SwiftUI 프로젝트와 Info.plist에 키가 없는 기존 앱은 모두 배타적입니다. 두 번째 아티클의 마이그레이션 메모는 여기서 나옵니다. 앱이 키를 NO로 설정했다면, 트래킹 루프를 교체할 때 “also set the view’s exclusiveGestureBehavior to NSView.ExclusiveGestureBehavior.exclusive”(뷰의 exclusiveGestureBehavior도 NSView.ExclusiveGestureBehavior.exclusive로 설정하라)이며, 이유는 “This prevents simultaneous gesture recognition with unrelated views, which the loop previously prevented implicitly.”(이렇게 하면 관련 없는 뷰와의 동시 제스처 인식을 막는데, 이전에는 루프가 이를 암묵적으로 막고 있었다)입니다.2
첫 번째 아티클은 배타성과 스크롤에 관여하는 세 설정을 “These settings don’t override each other.”(이 설정들은 서로를 덮어쓰지 않는다)라는 문장으로 정리합니다.1 exclusiveGestureBehavior와 plist 키는 AppKit이 어떤 인식기를 수집할지 정하며, “The view’s setting applies to that view and its subviews, and takes precedence over the app-wide key.”(뷰의 설정은 그 뷰와 서브뷰에 적용되고, 앱 전체 키보다 우선한다)1 isCancellableByScrollGesture는 인식기 단위이고 독립적입니다. “It lets a scroll gesture cancel that gesture recognizer even if it has already begun.”(이미 시작한 제스처 인식기라도 스크롤 제스처가 취소할 수 있게 한다)1 scrollGestureForRelationships는 스크롤 뷰 자체의 인식기와 경쟁하는 인식기를 위한 것입니다.1 아티클이 주의하라고 하는 시나리오는 스크롤 뷰 안의 버튼입니다. 버튼의 인식기에 isCancellableByScrollGesture를 true로 설정하면, 버튼에서 시작한 드래그가 클릭 대신 스크롤이 됩니다.1
텍스트 엔진에는 도구가 하나 더 있습니다. NSTextSelectionManager는 “creates its gesture recognizers itself, so you don’t have a reference to them”(제스처 인식기를 스스로 만들므로 그 참조를 가질 수 없다)이므로, 이를 기다려야 하는 커스텀 인식기는 관리자의 gesturesForFailureRequirements에 다른 인식기가 들어 있을 때 gestureRecognizer(_:shouldRequireFailureOf:)에서 true를 반환합니다.1 이 속성은 macOS 27.0으로 올라 있고, macOS 27.0 SDK의 NSTextSelectionManager.h 59행에 선언되어 있습니다(필자 분석).67
글머리표 5개로 보는 에뮬레이션
오후 글은 AppKit이 아닌 UI 프레임워크에 대한 6월 기술 노트의 에뮬레이션 규칙 4개를 인용했습니다. 아티클의 목록은 5개이며, 새로 생긴 것은 드래그입니다.18
- “Tapping emulates a mouse-down followed immediately by a mouse-up.”(탭은 마우스 다운과 곧바로 이어지는 마우스 업을 흉내 낸다)
- “Touching and then immediately panning emulates a trackpad scroll, even if the UI doesn’t otherwise handle Multi-Touch input directly.”(터치한 뒤 곧바로 팬하면, UI가 따로 멀티 터치 입력을 직접 처리하지 않더라도 트랙패드 스크롤을 흉내 낸다)
- “Touching, briefly holding, and then moving emulates mouseDown(with:), followed by mouseDragged(with:) as it moves, and mouseUp(with:) on lift.”(터치하고 잠깐 누른 뒤 움직이면 mouseDown(with:)을, 움직이는 동안 mouseDragged(with:)를, 손가락을 뗄 때 mouseUp(with:)을 흉내 낸다)
- “Touching and holding without movement emulates rightMouseDown(with:), and rightMouseUp(with:) on lift.”(움직이지 않고 길게 누르면 rightMouseDown(with:)을, 손가락을 뗄 때 rightMouseUp(with:)을 흉내 낸다)
- “Pinching or rotating with two fingers emulates the trackpad magnify and rotate gestures.”(두 손가락으로 핀치하거나 회전하면 트랙패드의 확대와 회전 제스처를 흉내 낸다)
세 번째 글머리표는 Tech Talk의 “drag with a small hold delay”(짧게 누른 뒤의 드래그)(11:31)를 글로 옮긴 것입니다. 곧바로 움직이는 터치는 스크롤하고, 먼저 누르고 있는 터치는 드래그합니다.9 6월의 목록에는 탭, 즉시 팬, 길게 누르기, 두 손가락 핀치나 회전이 있었고, 드래그 항목은 없었습니다.8 한계는 두 손가락입니다. “This emulation covers interactions with one or two fingers. If another finger touches down during a pinch or rotation, the emulation ignores it and keeps tracking the original two fingers.”(이 에뮬레이션은 한 손가락이나 두 손가락의 상호작용을 다룬다. 핀치나 회전 중에 다른 손가락이 닿으면 에뮬레이션은 이를 무시하고 원래의 두 손가락을 계속 추적한다)1 세 손가락 이상에는 인식기가 필요하며, 프레임워크가 터치를 직접 처리하게 되면 NSIsTouchNative를 YES로 설정해 추가 에뮬레이션을 끕니다.1
게임: TN3216
이 기술 노트는 게임을 위한 것입니다. “This guidance focuses on configurations common in games, such as an app with a single view backed by a CAMetalLayer.”(이 지침은 CAMetalLayer를 바탕으로 한 단일 뷰 앱처럼 게임에서 흔한 구성에 초점을 맞춘다)3 디자인 관점은 HIG보다 엄격합니다. “Touch extends the Mac experience rather than replacing other input”(터치는 다른 입력을 대체하는 것이 아니라 Mac 경험을 확장한다), “Touch works best for brief interactions that complement these devices”(터치는 이 기기들을 보완하는 짧은 상호작용에 가장 잘 맞는다), 그리고 “Design touch interactions for brief moments that the player can complete with one hand, while the other hand stays on the keyboard, mouse, or controller”(다른 손은 키보드, 마우스, 컨트롤러에 둔 채 한 손으로 마칠 수 있는 짧은 순간을 위해 터치 상호작용을 디자인하라)라는 지시가 있으며, 대상은 “such as inventory management, map zoom and scroll, or the settings menu”(인벤토리 관리, 지도 확대와 스크롤, 설정 메뉴 같은) 상호작용입니다.3
호환성 이야기는 같은 에뮬레이션에 게임용 변형이 더해진 것입니다. “AppKit applies game-specific emulation to apps in a games category, and can apply emulation to an individual view, such as one that hosts a third-party UI framework.”(AppKit은 게임 카테고리의 앱에 게임 전용 에뮬레이션을 적용하며, 서드파티 UI 프레임워크를 호스팅하는 뷰처럼 개별 뷰에 에뮬레이션을 적용할 수도 있다)3 카테고리는 Info.plist에서 읽습니다. LSApplicationCategoryType이 public.app-category.games이거나 게임 하위 카테고리이면 “the system applies game-specific mouse emulation”(시스템이 게임 전용 마우스 에뮬레이션을 적용한다)입니다.3 이것이 해결하는 문제는 마우스에는 결코 생기지 않는 문제입니다. “A mouse moves continuously from one position to the next, but a player can tap or start a drag at two distant locations in succession”(마우스는 한 위치에서 다음 위치로 연속해서 움직이지만, 플레이어는 멀리 떨어진 두 위치에서 연달아 탭하거나 드래그를 시작할 수 있다)이므로 델타를 누적하는 게임은 어긋나게 되고, “For games, AppKit sends a mouseMoved event to the new location before each mouseDown(with:), so your game can refresh its cached mouse position.”(게임에 대해 AppKit은 각 mouseDown(with:) 전에 새 위치로 mouseMoved 이벤트를 보내, 게임이 캐시한 마우스 위치를 갱신할 수 있게 한다)3
이어서 기술 노트는 에뮬레이션된 이벤트를 순서대로 나열합니다. 탭이라면 “from the last mouse position to the tap location”(마지막 마우스 위치에서 탭 위치까지)의 mouseMoved(with:), 그다음 mouseDown(with:), 그다음 mouseUp(with:)입니다. 드래그라면 드래그가 시작되는 위치로의 mouseMoved(with:), mouseDown(with:), “repeatedly while the touch continues”(터치가 계속되는 동안 반복해서) 오는 mouseDragged(with:), 그리고 mouseUp(with:)입니다.3 그리고 “Important” 상자에 규칙이 하나 있습니다. “Don’t intercept events by overriding sendEvent(:) or pulling them with nextEvent(matching:until:inMode:dequeue:). Doing so can interfere with gesture recognizers that respond to a physical mouse.”(sendEvent(:)를 오버라이드하거나 nextEvent(matching:until:inMode:dequeue:)로 이벤트를 끌어와 가로채지 마라. 그러면 실제 마우스에 반응하는 제스처 인식기를 방해할 수 있다)3
Tech Talk들에 대응하는 내용이 없는 부분은 GCMouse입니다. “By default, the system presents each touch surface, whether a Mac with a touch display or a Sidecar-connected iPad, as an additional GCMouse that emulates mouse input from touches”(기본적으로 시스템은 터치 디스플레이 Mac이든 Sidecar로 연결한 iPad든 각 터치 표면을, 터치로부터 마우스 입력을 에뮬레이션하는 추가 GCMouse로 제시한다)이며, 손가락을 대면 왼쪽 버튼이 눌리고, 움직이면 델타와 함께 마우스 이동 핸들러가 호출되며, 떼면 버튼이 놓입니다.3 한계는 이렇습니다. “The emulated GCMouse ignores additional fingers, and it doesn’t emulate other buttons, scrolling, or pinch input”(에뮬레이션된 GCMouse는 추가 손가락을 무시하고, 다른 버튼, 스크롤, 핀치 입력은 에뮬레이션하지 않는다), 그리고 터치는 서로 떨어진 곳에 닿기 때문에 “Don’t use them to track an absolute pointer position.”(이를 절대 포인터 위치 추적에 쓰지 마라)3
NSIsTouchNative는 AppKit 앱보다 게임에서 더 많은 일을 합니다. YES로 설정하면 “AppKit’s emulation will stick to a generic emulation - ignoring the app’s category or whether it uses a third-party UI framework - and disable GCMouse emulation”(AppKit의 에뮬레이션은 앱의 카테고리나 서드파티 UI 프레임워크 사용 여부를 무시하고 범용 에뮬레이션에 머물며, GCMouse 에뮬레이션을 끈다)이므로, 터치 표면은 더 이상 GCMouse로 나타나지 않고 “Your game receives touch through gesture recognizers instead of emulated mouse events.”(게임은 에뮬레이션된 마우스 이벤트 대신 제스처 인식기로 터치를 받는다)3 남는 것이 하나 있습니다. “AppKit still emulates mouse events for any NSView that handles them, for example by overriding mouseDown:.”(AppKit은 예를 들어 mouseDown:을 오버라이드해 마우스 이벤트를 처리하는 NSView에는 여전히 마우스 이벤트를 에뮬레이션한다)3 키 자체의 페이지는 대부분의 AppKit 앱에는 이 키가 필요 없으며, 이 키가 “primarily intended for apps that embed a non-AppKit rendering or input framework and implement their own touch-handling pipeline”(주로 AppKit이 아닌 렌더링 또는 입력 프레임워크를 내장하고 자체 터치 처리 파이프라인을 구현한 앱을 위한 것)이라고 말합니다. 게임 엔진이 바로 그 경우입니다.6
구현 섹션은 Swift와 Objective-C로 된 델리게이트 패턴입니다. 실제 마우스를 무시하도록 buttonMask를 0으로 둔 팬 인식기와 탭 인식기, 확대 인식기, 팬과 핀치 쌍에 대해서만 true를 반환하는 shouldRecognizeSimultaneouslyWith, 그리고 탭이 둘 다를 기다리게 하는 shouldRequireFailureOf입니다.3 노트 하나가 첫 번째 아티클이 말한 기본값을 확인해줍니다. “The gesture recognizers must include direct in allowedTouchTypes. On macOS 27 and later, this is the default.”(제스처 인식기의 allowedTouchTypes에는 direct가 포함되어야 한다. macOS 27 이상에서는 이것이 기본값이다)3 라우팅 조언은 엔진에 맞춘 것입니다. “Install the gesture recognizers once in your app and leave them enabled”(제스처 인식기는 앱에 한 번 설치하고 켜둔 채로 두라), 콜백을 엔진 입력 이벤트로 변환하라, 그리고 “Let the active input handler decide if and how to react to different gestures”(여러 제스처에 반응할지, 어떻게 반응할지는 활성 입력 핸들러가 정하게 하라)이며, 팬 핸들러는 백킹 스케일 팩터로 포인트를 픽셀로 바꾸고, 원점이 왼쪽 위인 엔진을 위해 y축을 뒤집습니다.3 표준 컨트롤은 전환 뒤에도 계속 동작합니다. 단 “as long as you don’t attach your own direct-touch gesture recognizers to them”(직접 만든 직접 터치 제스처 인식기를 거기에 붙이지 않는 한)입니다.3
마지막 체크리스트는 두 부분으로 나뉩니다. 에뮬레이션에 기대는 게임이라면 각 이벤트에서 위치를 읽고, sendEvent:를 오버라이드하거나 nextEventMatchingMask:에서 이벤트를 끌어오지 마십시오. 터치를 직접 처리하는 게임이라면 NSIsTouchNative를 설정하고, buttonMask를 0으로 둔 직접 터치 인식기를 쓰고, 플레이어가 기대하는 곳에 터치를 더하되 한 손으로 할 수 있게 하고, “Continue to read GCMouse for the physical mouse. With NSIsTouchNative set, touch no longer appears as an emulated GCMouse”(실제 마우스는 계속 GCMouse로 읽으라. NSIsTouchNative를 설정하면 터치는 더 이상 에뮬레이션된 GCMouse로 나타나지 않는다)를 따르며, 가능하면 표준 컨트롤을 쓰십시오.3
10월 28일자 HIG
오후 글은 랜딩 페이지가 링크할 예정이던 HIG 페이지 4개에 Mac 디스플레이의 터치에 관한 지침이 없다고 말했습니다. 이제 그중 3개에는 지침이 있고, 각각 2026년 10월 28일자 변경 이력 행이 붙어 있습니다. Live Activities 페이지는 바뀌지 않았습니다.4
Designing for macOS(“Added guidance for touch input.”)는 플랫폼 설명 세 줄을 다시 쓰고 모범 사례 하나를 더합니다. 인체공학 항목은 “On a Mac with a touch display, people reach forward to touch the screen, so they tend to use touch for brief, intentional interactions rather than sustained work.”(터치 디스플레이 Mac에서 사람들은 화면을 터치하려고 앞으로 손을 뻗으므로, 지속적인 작업보다는 짧고 의도적인 상호작용에 터치를 쓰는 경향이 있다)입니다. 입력 항목은 이제 키보드와 포인팅 장치를 앞에 두고, 그 밖의 입력 가운데 “Gestures on a trackpad or a Mac with a touch display”(트랙패드나 터치 디스플레이 Mac에서의 제스처)를 나열합니다. 새 모범 사례는 디자인 Tech Talk 전체를 한 문장에 담습니다. “Support standard gestures on trackpads and Mac devices with a touch display. Use larger tap targets and controls that expand on touch to make touch interaction easier, while keeping control size and density the same to stay optimized for keyboard and pointing device input.”(트랙패드와 터치 디스플레이 Mac 기기에서 표준 제스처를 지원하라. 터치 상호작용을 쉽게 하려면 더 큰 탭 타깃과 터치하면 확장되는 컨트롤을 쓰되, 키보드와 포인팅 장치 입력에 최적화된 상태를 유지하도록 컨트롤 크기와 밀도는 그대로 두라)4
Gestures(“Added guidance for gestures and touch input in macOS.”)는 macOS 섹션을 넓혔고, 이 섹션은 이제 개발자 지침으로 두 AppKit 아티클을 모두 링크합니다. 첫 문장이 순위를 정합니다. “On a Mac with a touch display, or on an iPad connected with Sidecar, people can also interact directly using touch gestures as a secondary input mode.”(터치 디스플레이 Mac이나 Sidecar로 연결한 iPad에서 사람들은 보조 입력 모드로서 터치 제스처를 써서 직접 상호작용할 수도 있다)4 이어서 굵은 글씨로 시작하는 규칙 6개가 있습니다.
- “Keep the layout and size of interface elements the same when you support touch.”(터치를 지원할 때 인터페이스 요소의 레이아웃과 크기를 그대로 유지하라) 이유는 “In macOS, touch is secondary to keyboard and pointing device input”(macOS에서 터치는 키보드와 포인팅 장치 입력에 비해 부차적이다)이고, 사람들이 입력을 자주 바꾸기 때문입니다. 내부 디스플레이가 size class를 바꾸는 iPhone Duo와의 대비가 핵심입니다. Mac은 입력이 바뀌고, 레이아웃은 바뀌지 않습니다.
- “Make tap targets bigger, not the controls themselves”(컨트롤 자체가 아니라 탭 타깃을 크게 하라). 방법은 “enlarging their tap targets to anticipate that interaction may start slightly outside of their visual bounds”(상호작용이 보이는 경계 조금 밖에서 시작될 수 있음을 예상해 탭 타깃을 넓히는 것)이고, “Where possible, use standard framework controls, which the system configures with an optimal tap target size automatically.”(가능하면 시스템이 최적의 탭 타깃 크기를 자동으로 설정해주는 표준 프레임워크 컨트롤을 쓰라)입니다.
- “Consider providing expandable controls”(확장 가능한 컨트롤 제공을 고려하라). 탭하면 나타나고 누르고 있는 동안 유지되며, “for a short time after they lift their finger to keep the control accessible to touch”(손가락을 뗀 뒤에도 컨트롤을 터치로 계속 쓸 수 있도록 잠시 동안) 남아 있는 일시적인 터치 크기 변형입니다. “When using standard system controls, steppers, sliders, and dials gain this behavior automatically.”(표준 시스템 컨트롤을 쓰면 스테퍼, 슬라이더, 다이얼은 이 동작을 자동으로 얻는다)
- “Don’t use touch as the only way to access controls or features”(터치를 컨트롤이나 기능에 접근하는 유일한 방법으로 쓰지 마라), 그리고 그 반대로 “aim to provide touch equivalents for actions otherwise performed exclusively with a pointing device, like using a keyboard modifier to shift-drag or displaying a menu when the pointer moves over an element.”(키보드 수정자로 Shift-드래그하거나 포인터가 요소 위로 움직일 때 메뉴를 표시하는 것처럼, 포인팅 장치로만 하던 동작에 대해 터치에 해당하는 방법을 제공하는 것을 목표로 하라)
- “Where possible, align with gestures people already know”(가능하면 사람들이 이미 아는 제스처에 맞추라). iPhone이나 iPad 앱, 또는 트랙패드의 제스처를 말하며, “Test to ensure they don’t conflict with macOS system gestures.”(macOS 시스템 제스처와 충돌하지 않는지 테스트하라)
- “Make sure custom gestures work for both touch and trackpad”(커스텀 제스처가 터치와 트랙패드 모두에서 동작하게 하라). 방법은 “modifier keys where needed to help the gesture work for all touch inputs”(필요한 곳에서 수정자 키를 써서 제스처가 모든 터치 입력에서 동작하도록 돕는 것)입니다.4
이 페이지는 36포인트라는 수치를 제시하지 않습니다. 그 숫자는 두 Tech Talk에 있습니다(디자인 Tech Talk 9:17의 “Touch on macOS uses a touch target of 36 points”, 그리고 AppKit Tech Talk 5:53의 히트 사각형 확장 “a minimum of 36 points on each axis”).9 페이지는 각 동작을 트랙패드 방식과 터치 방식으로 짝지은 표 “macOS trackpad and touch display gestures”를 더합니다. 선택은 기본 클릭 또는 탭, 열기는 더블 클릭 또는 더블 탭, 스크롤은 트랙패드에서는 두 손가락이고 디스플레이에서는 한 손가락, 확대와 회전은 양쪽 모두 같은 핀치와 두 손가락 돌리기, 컨텍스트 메뉴는 보조 클릭 또는 길게 누르기, 드래그는 클릭 후 드래그 또는 “Touch and hold, then drag”(길게 누른 뒤 드래그)입니다.4 플랫폼 간 표에서 “Touch (or pinch) and hold”는 이제 macOS를 나열하고, 더블 탭 행에는 “open an item on a Mac with a touch display”(터치 디스플레이 Mac에서 항목 열기)가 추가되었습니다.4
Pointing devices(“Added guidance for revealing hidden controls with touch on Mac, and updated guidance and artwork for iPadOS and macOS.”)는 가장 구체적인 규칙이 있는 페이지이며, 규칙은 새 제목 “Touch support for hidden controls” 아래에 있습니다. 전제는 이렇습니다. “Some Mac apps reveal certain controls only when the pointer moves over them. These controls need another way to appear when someone uses touch on a Mac that supports it.”(어떤 Mac 앱은 포인터가 위로 움직일 때만 특정 컨트롤을 드러낸다. 이 컨트롤들은 터치를 지원하는 Mac에서 누군가 터치를 쓸 때 나타날 다른 방법이 필요하다)4 규칙은 다음과 같습니다.
- “Decide which hidden controls need touch support. Reserve touch support for essential controls.”(어떤 숨겨진 컨트롤에 터치 지원이 필요한지 정하라. 터치 지원은 필수 컨트롤에만 두라) 툴팁은 해당하지 않고, 터치로 이미 다른 방법이 있는 것도 해당하지 않습니다. “someone can swipe to scroll a carousel, so there’s no need to reveal the navigation buttons that exist for mice without a scroll wheel.”(캐러셀은 스와이프로 스크롤할 수 있으므로, 스크롤 휠이 없는 마우스를 위해 있는 탐색 버튼을 드러낼 필요가 없다) 그리고 “Avoid keeping hidden controls visible all the time just to accommodate touch.”(터치에 맞추려고 숨겨진 컨트롤을 늘 보이게 두지 마라)
- “Reveal a hidden control when someone taps its general location on a touch display.”(누군가 터치 디스플레이에서 숨겨진 컨트롤의 대략적인 위치를 탭하면 그 컨트롤을 드러내라) 관련된 것만 드러내고, “Hide a control again when the person finishes with it, or after a few seconds if they don’t use it.”(사용자가 다 쓰면, 또는 쓰지 않으면 몇 초 뒤에 컨트롤을 다시 숨기라)
- “Retain the current selection when revealing a hidden control”(숨겨진 컨트롤을 드러낼 때 현재 선택을 유지하라). 컨트롤이 선택 항목에 작용할 수 있기 때문입니다.
- “Make essential hidden controls available in a context menu”(필수인 숨겨진 컨트롤을 컨텍스트 메뉴에서 쓸 수 있게 하라). 터치에서는 길게 눌러 컨텍스트 메뉴를 엽니다.
- “In a sidebar, reveal hidden controls when someone taps the corresponding section header or row.”(사이드바에서는 누군가 해당 섹션 헤더나 행을 탭하면 숨겨진 컨트롤을 드러내라) 예는 Finder입니다. “tapping the Favorites header in the Finder sidebar collapses the section and reveals a disclosure button.”(Finder 사이드바의 Favorites 헤더를 탭하면 섹션이 접히고 펼침 버튼이 드러난다) 드러난 컨트롤은 터치가 사이드바를 벗어나거나 포인팅 장치를 쓸 때까지 남아 있고, 숨겨진 컨트롤 영역을 탭하면 활성화하지 않고 드러내기만 합니다. “The control acts only when they tap it again.”(컨트롤은 다시 탭해야만 동작한다)
- “In a tabbed window, reveal the selected tab’s close button when someone taps anywhere in the window”(탭이 있는 윈도우에서는 누군가 윈도우 아무 곳이나 탭하면 선택된 탭의 닫기 버튼을 드러내라), 그리고 사용자가 포인팅 장치로 돌아갈 때까지 유지하십시오.4
이 규칙 6개는 두 Tech Talk의 호버 지침을 글로 옮긴 것이며, Tech Talk가 주지 않았던 시간과 예외가 들어 있습니다. 드러난 컨트롤이 숨기 전의 몇 초, 활성화를 위한 두 번째 탭, 그리고 계속 보이는 경우인 사이드바와 탭 닫기 버튼입니다. 같은 개정에서 이 페이지는 iPadOS 포인터 내용을 Liquid Glass에 맞게 다시 썼고, 본문의 기본 포인터 모양을 원에서 화살표로 바꿨으며, 커서 표의 AppKit 전용 열을 AppKit과 SwiftUI 심볼로 바꿨고, 포인터 자석 효과 섹션을 없앴습니다. 이 중 어느 것도 Mac 디스플레이에 관한 것은 아닙니다.4
아직 없는 것
10월 28일 오후 9시 51분(PT)에 Apple 문서 엔드포인트, 랜딩 페이지, 개발자 뉴스와 릴리스 피드, Tech Talks 목록과 대조해 확인했습니다.56
- Xcode 27.3 베타. 랜딩 페이지 타일은 여전히 “Coming soon”이고, 릴리스 피드에는 10월 26일 포인트 릴리스(macOS 27.2도 그중 하나)보다 새로운 것이 없으며, Xcode 27.3과 macOS 27.3 릴리스 노트 페이지는 404를 반환합니다. 가장 새로운 Xcode는 여전히 27.2 RC이고, 그 SDK는 살펴보지 않았습니다. 오후 글의 최소 배포 버전에 관한 단서는 여전히 유효하며, 최소 배포 버전에 관한 배경은 Xcode 27 릴리스 글에 있습니다.
- AppKit Tech Talk가 시연한 시스템 제스처 스위치 3개, 즉
NSWindow.disablesDirectTouchSystemGestures,UIViewController.prefersDirectTouchSystemGesturesDisabled, SwiftUI의directTouchSystemGesturesDisabled(). 세 가지 모두 404를 반환하고, 새 아티클 어느 쪽도 이를 언급하지 않습니다. 아티클은 시스템 제스처가 인식기를 어떻게 취소하는지는 설명하지만, 시스템 제스처를 끄는 방법은 설명하지 않습니다. - macOS에서 쓸 수 있는 ActivityKit. 프레임워크 페이지는 10월 8일 스탬프를 달고 macOS 플랫폼을 나열하지 않으며, Live Activities Tech Talk의
AlertConfiguration.announcement(_:)는 404를 반환합니다. 오후 글의 Live Activities 섹션은 쓰인 그대로 유효하고, 상태 머신 글은 여전히 iOS 구현만 다룹니다. - AppKit Tech Talk가 12:16에 언급한 “gesture modernization skill”(제스처 현대화 스킬). 이 이름을 밝힌 Apple 페이지는 없고, 랜딩 페이지의
TK skill주석은 그대로 있습니다.59 - Tech Talk 페이지에서 아티클로 가는 링크, 그리고 Mac 터치를 위한 시뮬레이터나 Device Hub 경로에 관한 Apple의 어떤 언급.
- SwiftUI 문서 업데이트 페이지의 날짜가 붙은 항목. 서버 스탬프는 10월 28일 오후 2시 58분(PT)이지만, 최신 섹션은 여전히 2026년 9월이고 제스처 항목은 2026년 6월의
GestureInputKinds줄입니다. 무엇이 바뀌었든, 날짜가 붙은 새 섹션은 아니었습니다.6
개발자 글에 대한 정정
오후 글은 정오의 확인 기준으로는 정확했습니다. 그 글의 진술 가운데 다섯 개는 10월 28일 오후 9시 51분(PT) 확인에서 더 이상 성립하지 않았습니다. 이 글과 함께 공개된 그 글의 업데이트 블록이 이 글을 링크하고, 해당 진술은 그 자리에서 정정되었습니다. 이것이 그 목록입니다.
- 랜딩 페이지의 “Coming soon” 타일 3개는 이제 하나입니다. 문서 자료와 디자인 자료는 공개되었고, Xcode 27.3 베타는 아닙니다.5
- “Adopting gesture recognizers in your app”은 더 이상 404를 반환하지 않습니다. 공개되어 있으며, 마이그레이션 지침은 이 문서를 기준으로 읽어야 합니다.1
- HIG 페이지는 더 이상 Mac 디스플레이의 터치에 대해 침묵하지 않습니다. 4개 중 3개에 10월 28일자 변경 이력 행이 있고, Live Activities 페이지에는 없습니다.4
- 에뮬레이션 목록의 규칙은 6월 기술 노트의 4개가 아니라 5개이며, 누른 뒤 드래그하는 규칙이 추가되었습니다.18
- 배타적 뷰가 기본으로 켜지는 것은 Info.plist에 키가 없는 앱에 한해서입니다. Xcode 27.1과 27.2 베타에서 Xcode의 macOS App 및 Document App 템플릿은 앱 라이프사이클이 AppKit 앱 델리게이트인 프로젝트에 대해 이 키를
NO로 씁니다.127
SDK, 문서화되지 않은 스위치 3개, ActivityKit에 대해 오후 글이 말한 내용은 그 뒤에 공개된 어떤 것으로도 바뀌지 않았습니다.
핵심 요약
AppKit 개발자에게: Sidecar에서 아티클의 네 줄 테스트를 실행한 다음, 아티클이 이름을 든 심볼 5개를 검색하십시오. 컨트롤 서브클래스에서 mouseDown(with:), mouseDragged(with:), mouseUp(with:) 중 하나를 오버라이드하면 super를 호출하든 하지 않든 그 컨트롤은 트래킹 루프로 떨어지며, 해결책은 숨김 집합에 .trackingEndedOutside를 넣은 NSControl.Events의 타깃-액션 쌍입니다.1
유지하는 루프에: 터치 전용 인식기에는 buttonMask 0이 필요합니다. 그렇지 않으면 마우스 드래그가 인식기를 시작시키고 mouseDown(with:)은 실행되지 않습니다. allowedTouchTypes = .direct는 이미 기본값입니다.1
커스텀 인식기에: 종료 상태에 도달하게 하고, reset()과 location(in:)을 오버라이드하고, -NSGestureRecognizerCrashOnMissingOverrides YES와 -NSCrashOnStuckGestureTimeout 2로 디버깅하되 배포 전에 제거하십시오. 프로덕션에서는 멈춘 인식기가 사용자가 상호작용을 멈추고 2초 뒤에 취소됩니다.12
배타성에: 키가 없는 앱은 배타적입니다. Xcode의 macOS App 템플릿에서 AppKit 앱 델리게이트 라이프사이클로 만든 프로젝트(모든 Storyboard나 XIB 프로젝트)는 배타적이지 않고, 기본 SwiftUI 라이프사이클로 만든 프로젝트는 배타적입니다. 트래킹 루프를 옮기기 전에 Info.plist를 읽으십시오.27
게임에: 델타를 더하지 말고 이벤트에서 위치를 읽으십시오. sendEvent(_:)를 오버라이드하지 말고, NSIsTouchNative가 YES가 되기 전까지는 각 터치 표면이 추가로 에뮬레이션된 GCMouse라는 점을 알아두십시오.3
디자이너에게: 터치는 “a secondary input mode”(보조 입력 모드)입니다. 레이아웃과 컨트롤 크기는 그대로 두고, 컨트롤이 아니라 탭 타깃을 넓히십시오. 숨겨진 컨트롤은 그 근처를 탭하면 드러내고, 사용자가 다 쓰면 또는 쓰지 않은 채 몇 초가 지나면 숨기되 사이드바와 탭 닫기 버튼은 예외로 하며, 필수 컨트롤에는 컨텍스트 메뉴를 주십시오.4
자주 묻는 질문
AppKit 마이그레이션 아티클은 공개되었습니까?
그렇습니다. “Adopting gesture recognizers in your app”은 10월 28일 정오(PT)에 404를 반환했고, 오후 9시 51분에는 공개되어 AppKit의 Essentials와 Gestures 섹션에 올라 있었으며, 랜딩 페이지의 문서 타일에서 링크되어 있었습니다.15
제 앱에서 배타적 뷰가 켜져 있습니까?
Info.plist에 NSViewGestureRecognizerIsExclusive 키가 없다면 켜져 있습니다. “If your app’s information property list doesn’t include the NSViewGestureRecognizerIsExclusive key, the system treats it as YES.”(앱의 정보 속성 목록에 NSViewGestureRecognizerIsExclusive 키가 없으면 시스템은 이를 YES로 취급한다)2 Xcode 27.1 또는 27.2 베타의 macOS App 템플릿에서 AppKit 앱 델리게이트 라이프사이클로 만든 프로젝트(모든 Storyboard나 XIB 프로젝트)는 이 키를 값이 NO인 빌드 설정으로 받고, 같은 템플릿에서 기본 SwiftUI 라이프사이클로 만든 프로젝트는 키를 쓰지 않습니다(이 Mac의 템플릿에 대한 필자 분석).7
끝나지 않는 제스처 인식기는 어떻게 됩니까?
시스템이 취소할 때까지 배타적 뷰를 붙잡고 있습니다. 인식기는 “is stuck if it hasn’t reached a terminal state within 2 seconds after the person stops interacting with your app”(사용자가 앱과의 상호작용을 멈춘 뒤 2초 안에 종료 상태에 도달하지 않으면 멈춘 것)이며, 시스템이 “automatically transitions it to the NSGestureRecognizer.State.cancelled state”(자동으로 NSGestureRecognizer.State.cancelled 상태로 전환한다)입니다.2 디버깅하는 동안 NSCrashOnStuckGestureTimeout 기본값을 설정하면 대신 스택 트레이스를 얻을 수 있습니다.2
팬 인식기를 추가한 뒤 트래킹 루프가 드래그를 받지 못하는 이유는 무엇입니까?
인식기의 buttonMask가 기본값이어서 기본 마우스 버튼을 가져가기 때문입니다. “A mouse drag then begins the gesture recognizer, and AppKit doesn’t call your mouseDown(with:) override, so the tracking loop doesn’t run.”(그러면 마우스 드래그가 제스처 인식기를 시작시키고, AppKit은 mouseDown(with:) 오버라이드를 호출하지 않으므로 트래킹 루프가 실행되지 않는다) 터치 전용 인식기에는 buttonMask를 0으로 설정하십시오.1
게임은 Mac에서 터치를 어떻게 받습니까?
문서화된 순서의 에뮬레이션된 마우스 이벤트로 받으며, 각 mouseDown(with:) 앞에는 새 위치로의 mouseMoved(with:)가 옵니다. 그리고 델타가 연속적이지 않은 추가 GCMouse로도 받습니다. NSIsTouchNative를 YES로 설정하면 게임은 제스처 인식기로 전환되고, 에뮬레이션된 GCMouse는 사라집니다.3
HIG는 Mac의 터치 타깃 크기를 제시합니까?
아닙니다. Gestures 페이지는 “Make tap targets bigger, not the controls themselves”(컨트롤 자체가 아니라 탭 타깃을 크게 하라)라고 말하고, 크기는 프레임워크 컨트롤에 맡깁니다. 36포인트라는 수치는 HIG가 아니라 두 Tech Talk에 있습니다.49
이 글의 사실은 어디에서 왔습니까?
Apple의 AppKit 아티클 2개, TN3216, HIG 페이지 3개, 랜딩 페이지이며, 모두 2026년 10월 28일 오후 9시 51분(PT)에 가져왔습니다. 아티클의 공개 여부, HIG 페이지, 랜딩 페이지는 같은 날 정오와 오전 11시 49분의 확인과 비교했습니다. 여기에 이름을 든 모든 심볼의 Apple 문서 JSON, 비교를 위한 6월의 기술 노트, Tech Talk 인용을 위한 영어 자막 트랙, 그리고 제 Mac에 있는 Xcode 27.1과 Xcode 27.2 베타 안의 헤더와 프로젝트 템플릿을 더했으며, 이는 필자 분석입니다. 언론 보도는 출처가 아닙니다.
출처
-
Apple Developer Documentation, Adopting gesture recognizers in your app, AppKit. 2026년 10월 28일 오후 9시 51분(PT) 조회(HTTP 200,
Last-Modified: Wed, 28 Oct 2026 21:30:45 GMT). 같은 날 정오(PT)에는 같은 URL과 그 JSON 엔드포인트가 404를 반환했습니다. AppKit 프레임워크 페이지의 Essentials 섹션에서 “Adopting Liquid Glass” 다음 두 번째로, 그리고 Gestures의 “Gesture handling” 아래에 올라 있습니다. Wayback Machine의 첫 캡처는 2026-10-29 01:08:12 UTC입니다(CDX 행20261029010812, 상태 200, 원본 헤더와 CDX 근거는 이 글의 증거 자료와 함께 보관). 인용한 섹션: Overview, “Prepare your app for a Mac with a touch display”(표, 네 줄 테스트, 검색 목록), “Filter gesture recognizer candidates”, “Handle touch input”, “Update your event-handling code”, “Replace common mouse-down overrides”, “Handle tracking-loop compatibility”, “Replace your custom tracking loops with gesture recognizers”, “Add touch support to your custom tracking loop”, “Write a custom gesture recognizer”, “Account for gesture recognizer behavior changes”, “Support non-AppKit UI frameworks”. 이 글의 Swift 코드 목록은 아티클의 “Add touch support to your custom tracking loop” 코드 목록을 네 줄로 줄인 것입니다. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Apple Developer Documentation, Handling AppKit gestures, AppKit. 2026년 10월 28일 오후 9시 51분(PT) 조회(
Last-Modified: Wed, 28 Oct 2026 05:35:41 GMT). 페이지에 개정 이력 없음, TN3212에서 링크되지 않음, 그 시점 Wayback Machine 인덱스에 캡처 없음. 인용한 섹션: Overview, “Track gesture recognizer states”, “Understand gesture recognizer gathering”, “Implement a custom gesture recognizer”, “Prioritize gesture recognizers”, “Delay responder events”, “Configure exclusive views”, “Replace event-tracking loops”, “Diagnose unresponsive gesture recognizers”. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Apple Developer Documentation, TN3216: Supporting touch input for games on macOS, 개정 이력 “2026-10-28 First published”, 2026년 10월 28일 오후 9시 51분(PT) 조회(
Last-Modified: Wed, 28 Oct 2026 21:09:50 GMT). 인용한 섹션: Overview, “Extend the player experience meaningfully”, “Understand automatic touch support in existing games”, “Understand GCMouse emulation”, “Opt out of mouse emulation”, “Implement touch events”, “Route gestures in your game engine”, “Use standard AppKit controls for automatic touch support”, “Review your touch integration”. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Apple Developer, Human Interface Guidelines, 2026년 10월 28일 오후 9시 51분(PT)에 가져와 같은 날 정오(PT)에 가져온 사본과 비교: Designing for macOS, 변경 이력 “October 28, 2026 | Added guidance for touch input.”; Gestures, 변경 이력 “October 28, 2026 | Added guidance for gestures and touch input in macOS.”, macOS 플랫폼 섹션과 표 “macOS trackpad and touch display gestures” 포함; Pointing devices, 변경 이력 “October 28, 2026 | Added guidance for revealing hidden controls with touch on Mac, and updated guidance and artwork for iPadOS and macOS.”, 섹션 “Touch support for hidden controls” 포함; Live Activities, 정오 사본과 바이트 단위로 동일, 변경 이력은 2026년 10월 13일에서 끝남. 앞의 세 페이지의 정오 사본에는 인용한 텍스트가 전혀 없습니다. 새 Gestures JSON의 서버
Last-Modified헤더는Tue, 27 Oct 2026 05:11:02 GMT이고, 오후 9시 59분의 첫 요청에서X-Cache: hit-stale이었습니다. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Apple Developer, Get ready for Touch on Mac, 2026년 10월 28일 오전 11시 49분(PT) 조회(“Coming soon”으로 표시된 타일 3개, 링크는 HTML 주석 안) 및 오후 9시 51분(PT) 조회: “Xcode 27.3 beta” / “Download the latest SDKs to build, run, and test your app.” / “Coming soon”; 아티클과 샘플을 링크하는 “Documentation resources” / “Learn how to build and optimize your app for touch input.”; Gestures, Designing for macOS, Pointing devices, 그리고 제품 베젤 목록에 “MacBook Pro PROLED XDR Display”가 있는 Apple Design Resources를 링크하는 “Design resources” / “Create experiences that take advantage of touch user interface patterns.”; 주석
TK skill은 두 번 모두 있음. Latest News RSS, 최신 항목 “Say hello to Touch on Mac”, Wed, 28 Oct 2026 10:00:55 PDT; Releases RSS, 최신 항목 날짜 Mon, 26 Oct 2026 10:00:00 PDT(iOS, iPadOS, macOS 27.2(26B123), tvOS, visionOS, watchOS 27.2); 111456, 111467, 111468을 나열하고 터치나 Live Activities에 관한 다른 Tech Talk는 없는 Tech Talks 목록; 어느 아티클로도 링크가 없는 Bring Touch to your Mac app 페이지; 모두 오후 9시 51분(PT) 조회. ↩↩↩↩↩↩↩↩↩ -
Apple 문서 JSON(
developer.apple.com/tutorials/data/documentation/<path>.json), 2026년 10월 28일 오후 9시 51분(PT) 조회. 찾은 것:trackingEndedOutside(macOS 11.0),NSEvent.isTouchSwipeNavigationEnabled(macOS 27.0),NSTextSelectionManager.gesturesForFailureRequirements(macOS 27.0), NSGestureRecognizerDelegate의gestureRecognizer(_:shouldRequireFailureOf:)(macOS 10.10),NSViewGestureRecognizerIsExclusive(“The default value isYES.”),NSIsTouchNative, Mouse, Keyboard, and Trackpad(리스폰더 체인과 터치에 관한 두 문장), Supporting drag and drop through file promises(직접 터치에 관한 문장), SwiftUI updates(Last-Modified: Wed, 28 Oct 2026 21:58:06 GMT, 최신 섹션 “September 2026”). 찾을 수 없음(HTTP 404):appkit/nswindow/disablesdirecttouchsystemgestures,uikit/uiviewcontroller/prefersdirecttouchsystemgesturesdisabled,swiftui/view/directtouchsystemgesturesdisabled(),activitykit/alertconfiguration/announcement(_:),xcode-release-notes/xcode-27_3-release-notes,macos-release-notes/macos-27_3-release-notes. ActivityKit:Last-Modified: Thu, 08 Oct 2026 15:31:35 GMT, 플랫폼 iOS, iPadOS, Mac Catalyst. ↩↩↩↩↩↩↩↩↩↩↩ -
2026년 10월 28일 SDK 및 템플릿 확인(필자 분석), Xcode 27.1(27A9275)에 포함된
MacOSX27.0.sdk:NSControl.h180행은NSControlEventTrackingEndedOutside = 1 << 7이며, Xcode 27.2 베타(27B5019j)에 포함된MacOSX27.2.sdk에도 같은 줄이 있습니다.NSEvent.h496행은@property (class, readonly, getter=isTouchSwipeNavigationEnabled) BOOL touchSwipeNavigationEnabled API_AVAILABLE(macos(27));이고,NSTextSelectionManager.h59행은gesturesForFailureRequirements를 선언하며,NSGestureRecognizer.h116행과 149행은shouldRequireFailureOfGestureRecognizer:를 선언합니다. 프로젝트 템플릿은 Xcode 27.1(27A9275)과 Xcode 27.2 베타(27B5019j)에서 plistlib로 읽었으며, 위치는Platforms/MacOSX.platform/Developer/Library/Xcode/Templates/Project Templates/macOS/Application/입니다.Cocoa App Base.xctemplate/TemplateInfo.plist는 키INFOPLIST_KEY_NSViewGestureRecognizerIsExclusive를 문자열 값NO로Options[0] (appLifecycle) / Units / Cocoa / Targets[0] / SharedSettings에 두며, 다른 곳에는 없습니다.Project Templates/macOS/Base/아래의macOS App Lifecycle.xctemplate는appLifecycle옵션을Name없이,Typepopup,DefaultSwiftUI,ValuesSwiftUI,Cocoa로 정의합니다.macOS Application UI.xctemplate는userInterface를NameInterface:,DefaultSwiftUI,ValuesSwiftUI,Storyboard,XIB로 정의하며, 그Storyboard와XIB유닛은SwiftUI유닛 하나와 마찬가지로appLifecycleCocoa를 담은RequiredOptions를 가집니다.App.xctemplate는Core Data App,macOS SwiftUI App,macOS Application UI,Cocoa App Native Base를 거쳐Cocoa App Base에서 내려옵니다.Cocoa Document App.xctemplate는Cocoa라이프사이클 유닛 안에서, 정의가<key>NSViewGestureRecognizerIsExclusive</key><false/>인Info.plist:NSViewGestureRecognizerIsExclusive노드를 씁니다.Game.xctemplate는appLifecycle을OverrideCocoa로 설정합니다.Document App.xctemplate는 (Core Data Document App,macOS SwiftUI Document-Based App,macOS Application UI,Cocoa App Native Base를 거쳐)Cocoa Document App과Cocoa App Base양쪽에서 내려옵니다. 두 Xcode 모두에서 이 키를 담은 템플릿 파일은 이 두 개뿐입니다. Xcode의INFOPLIST_KEY_빌드 설정은 생성된 Info.plist에 기록됩니다. ↩↩↩↩↩↩↩↩↩↩ -
Apple Developer Documentation, TN3212: Adopting gesture recognizers for Sidecar touch support, 개정 이력 “2026-06-08 First published”, 섹션 “Support third-party UI frameworks”, 글머리표 4개: “A tap emulates a mouse-down followed immediately by a mouse-up”, “A touch and immediate pan emulates trackpad scroll events, even if the UI doesn’t respond to them”, “A long press without movement emulates a rightMouseDown(with:) (optionally followed by rightMouseDragged(with:)) with a rightMouseUp(with:) on touch lift”, “A two-finger pinch or rotation simultaneously emulates both the trackpad magnify and rotate gestures”. 2026년 10월 28일 조회. ↩↩↩
-
Apple Developer, Tech Talk Bring Touch to your Mac app(13:19, Raleigh Ledet)과 Design for touch on Mac(16:52, Cas Lemmens), 2026년 10월 28일. 인용은 영어 자막 트랙에서 가져왔으며, 괄호 안의 시각은 인용 구간의 첫 큐입니다: “drag with a small hold delay”(111468, 11:31), “there’s a gesture modernization skill that has an understanding of the features and changes I’ve talked about”(111468, 12:16), “Touch on macOS uses a touch target of 36 points”(111456, 9:17), “a minimum of 36 points on each axis”(111468, 5:53). Tech Talk 페이지의 코드 목록 “Use NSControl events to detect tracking”(2:24)은
[.trackingBegan, .trackingEntered],[.trackingExited, .trackingEndedInside],.trackingCancelled에 대해addTarget(_:action:for:)를 등록합니다. ↩↩↩↩↩