|
| 1 | +<!DOCTYPE html> |
| 2 | +<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"> |
| 3 | +<title>π£οΈ Build a Siri Voice Control App β HIG Lab</title> |
| 4 | +<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700;900&family=JetBrains+Mono&display=swap" rel="stylesheet"> |
| 5 | +<style> |
| 6 | +:root{--bg:#fafafa;--text:#1d1d1f;--text-2:#6e6e73;--accent:#0071e3;--code-bg:#1e1e2e;--border:#d2d2d7;--green:#34c759} |
| 7 | +*{margin:0;padding:0;box-sizing:border-box}body{font-family:'Inter',sans-serif;background:var(--bg);color:var(--text);line-height:1.8} |
| 8 | +.top-bar{position:sticky;top:0;z-index:100;background:rgba(250,250,250,0.9);backdrop-filter:blur(20px);border-bottom:1px solid var(--border)} |
| 9 | +.top-bar-inner{max-width:800px;margin:0 auto;padding:12px 24px;display:flex;justify-content:space-between} |
| 10 | +.top-logo{font-size:16px;font-weight:700;text-decoration:none;color:var(--text)}.top-logo span{color:var(--accent)} |
| 11 | +article{max-width:800px;margin:0 auto;padding:48px 24px 120px} |
| 12 | +h1{font-size:clamp(28px,5vw,40px);font-weight:900;margin-bottom:16px;letter-spacing:-1px} |
| 13 | +h2{font-size:24px;font-weight:800;margin:48px 0 16px}p{margin-bottom:16px} |
| 14 | +.subtitle{font-size:18px;color:var(--text-2);margin-bottom:32px} |
| 15 | +.code-block{background:var(--code-bg);border-radius:12px;margin:20px 0;overflow:hidden} |
| 16 | +.code-header{padding:10px 16px;background:rgba(255,255,255,0.05);border-bottom:1px solid rgba(255,255,255,0.08)} |
| 17 | +.code-filename{font-size:13px;color:#cdd6f4;font-family:'JetBrains Mono',monospace} |
| 18 | +pre.code-body{margin:0;padding:16px;font-family:'JetBrains Mono',monospace;font-size:13px;line-height:1.6;color:#cdd6f4;white-space:pre;overflow-x:auto} |
| 19 | +.kw{color:#cba6f7}.type{color:#89b4fa}.str{color:#a6e3a1}.func{color:#89dceb}.prop{color:#f5c2e7}.comment{color:#6c7086} |
| 20 | +.tip-box{background:#34c75910;border:1px solid #34c75940;border-radius:12px;padding:16px 20px;margin:24px 0} |
| 21 | +.tip-box h4{color:var(--green);margin-bottom:8px} |
| 22 | +</style></head> |
| 23 | +<body> |
| 24 | +<div class="top-bar"><div class="top-bar-inner"><a href="../index.html" class="top-logo">HIG <span>Lab</span></a><a href="01-siri-todo-app.html" class="lang-toggle" style="font-size:12px;font-weight:600;color:#0071e3;text-decoration:none;padding:4px 10px;border:1.5px solid #0071e3;border-radius:12px;">π KO</a></div></div> |
| 25 | +<article> |
| 26 | +<h1>π£οΈ Siri μμ± μ μ΄ μ± λ§λ€κΈ°</h1> |
| 27 | +<!-- higlab-metadata --> |
| 28 | +<div style="display:flex;gap:16px;align-items:center;margin:12px 0 24px;padding:12px 16px;background:var(--tag-bg,#f5f5f7);border-radius:10px;font-size:13px;color:var(--text-2,#6e6e73);flex-wrap:wrap;"> |
| 29 | + <span>β Difficulty: βββ</span> |
| 30 | + <span>β±οΈ Est. Time: 2-3h</span> |
| 31 | + <span>π App Frameworks</span> |
| 32 | +</div> |
| 33 | +<p class="subtitle">Build an app that works when you say "Add a todo" using App Intents.</p> |
| 34 | + |
| 35 | +<h2>β¨ App Intents?</h2> |
| 36 | +<p>App Intents exposes your app's features to Siri, Shortcuts, and Spotlight. Available on iOS 16+ and much simpler than the previous SiriKit.</p> |
| 37 | + |
| 38 | +<h2>π 첫 λ²μ§Έ Intent λ§λ€κΈ°</h2> |
| 39 | +<div class="code-block"><div class="code-header"><span class="code-filename">AddTodoIntent.swift</span></div> |
| 40 | +<pre class="code-body"><span class="kw">import</span> <span class="type">AppIntents</span> |
| 41 | + |
| 42 | +<span class="kw">struct</span> <span class="type">AddTodoIntent</span>: <span class="type">AppIntent</span> { |
| 43 | + <span class="kw">static var</span> <span class="prop">title</span>: <span class="type">LocalizedStringResource</span> = <span class="str">"ν μΌ μΆκ°"</span> |
| 44 | + |
| 45 | + @<span class="type">Parameter</span>(title: <span class="str">"μ λͺ©"</span>) |
| 46 | + <span class="kw">var</span> <span class="prop">todoTitle</span>: <span class="type">String</span> |
| 47 | + |
| 48 | + <span class="kw">func</span> <span class="func">perform</span>() <span class="kw">async throws</span> -> <span class="kw">some</span> <span class="type">IntentResult</span> { |
| 49 | + <span class="type">TodoStore</span>.shared.add(title: todoTitle) |
| 50 | + <span class="kw">return</span> .result(dialog: <span class="str">"\(todoTitle) μΆκ° μλ£!"</span>) |
| 51 | + } |
| 52 | +}</pre></div> |
| 53 | + |
| 54 | +<h2>π€ Siri μμ± λͺ
λ Ή λ±λ‘</h2> |
| 55 | +<div class="code-block"><div class="code-header"><span class="code-filename">Shortcuts.swift</span></div> |
| 56 | +<pre class="code-body"><span class="kw">struct</span> <span class="type">TodoShortcuts</span>: <span class="type">AppShortcutsProvider</span> { |
| 57 | + <span class="kw">static var</span> <span class="prop">appShortcuts</span>: [<span class="type">AppShortcut</span>] { |
| 58 | + <span class="type">AppShortcut</span>( |
| 59 | + intent: <span class="type">AddTodoIntent</span>(), |
| 60 | + phrases: [ |
| 61 | + <span class="str">"ν μΌ μΆκ°ν΄μ€ \(.applicationName)"</span>, |
| 62 | + <span class="str">"\(.applicationName)μ ν μΌ μΆκ°"</span> |
| 63 | + ], |
| 64 | + shortTitle: <span class="str">"ν μΌ μΆκ°"</span>, |
| 65 | + systemImageName: <span class="str">"plus.circle"</span> |
| 66 | + ) |
| 67 | + } |
| 68 | +}</pre></div> |
| 69 | + |
| 70 | +<div class="tip-box"> |
| 71 | +<h4>π‘ HIG ν</h4> |
| 72 | +<p>Write phrases as natural sentences β use expressions people would actually say, like "Add a todo" or "Create a new task."</p> |
| 73 | +</div> |
| 74 | + |
| 75 | +<h2>π νλΌλ―Έν° λν νλ¦</h2> |
| 76 | +<p>νλΌλ―Έν°κ° μμΌλ©΄ Siriκ° μλμΌλ‘ λ¬Όμ΄λ΄
λλ€:</p> |
| 77 | +<div class="code-block"><div class="code-header"><span class="code-filename">Dialog.swift</span></div> |
| 78 | +<pre class="code-body"><span class="kw">guard let</span> title = todoTitle <span class="kw">else</span> { |
| 79 | + <span class="kw">throw</span> $todoTitle.<span class="func">needsValueError</span>(<span class="str">"μ΄λ€ ν μΌμ μΆκ°ν κΉμ?"</span>) |
| 80 | +}</pre></div> |
| 81 | + |
| 82 | +<h2>π Entity μ μ</h2> |
| 83 | +<p>Define todo items as Entities so Siri can suggest and search tasks:</p> |
| 84 | +<div class="code-block"><div class="code-header"><span class="code-filename">TodoEntity.swift</span></div> |
| 85 | +<pre class="code-body"><span class="kw">struct</span> <span class="type">TodoEntity</span>: <span class="type">AppEntity</span> { |
| 86 | + <span class="kw">static var</span> <span class="prop">typeDisplayRepresentation</span>: <span class="type">TypeDisplayRepresentation</span> { |
| 87 | + <span class="str">"ν μΌ"</span> |
| 88 | + } |
| 89 | + |
| 90 | + <span class="kw">var</span> <span class="prop">id</span>: <span class="type">UUID</span> |
| 91 | + <span class="kw">var</span> <span class="prop">title</span>: <span class="type">String</span> |
| 92 | + <span class="kw">var</span> <span class="prop">isCompleted</span>: <span class="type">Bool</span> |
| 93 | + |
| 94 | + <span class="kw">var</span> <span class="prop">displayRepresentation</span>: <span class="type">DisplayRepresentation</span> { |
| 95 | + <span class="type">DisplayRepresentation</span>(title: <span class="str">"\(title)"</span>) |
| 96 | + } |
| 97 | + |
| 98 | + <span class="comment">// Spotlight κ²μ μ§μ</span> |
| 99 | + <span class="kw">static var</span> <span class="prop">defaultQuery</span> = <span class="type">TodoEntityQuery</span>() |
| 100 | +}</pre></div> |
| 101 | + |
| 102 | +<h2>π EntityQuery ꡬν</h2> |
| 103 | +<div class="code-block"><div class="code-header"><span class="code-filename">TodoEntityQuery.swift</span></div> |
| 104 | +<pre class="code-body"><span class="kw">struct</span> <span class="type">TodoEntityQuery</span>: <span class="type">EntityQuery</span> { |
| 105 | + <span class="kw">func</span> <span class="func">entities</span>(<span class="kw">for</span> identifiers: [<span class="type">UUID</span>]) <span class="kw">async throws</span> -> [<span class="type">TodoEntity</span>] { |
| 106 | + <span class="type">TodoStore</span>.shared.todos.filter { identifiers.contains($0.id) } |
| 107 | + } |
| 108 | + |
| 109 | + <span class="kw">func</span> <span class="func">suggestedEntities</span>() <span class="kw">async throws</span> -> [<span class="type">TodoEntity</span>] { |
| 110 | + <span class="comment">// λ―Έμλ£ νλͺ© 3κ° μ μ</span> |
| 111 | + <span class="type">TodoStore</span>.shared.todos |
| 112 | + .filter { !$0.isCompleted } |
| 113 | + .prefix(<span class="num">3</span>) |
| 114 | + .map(<span class="type">TodoEntity</span>.<span class="kw">init</span>) |
| 115 | + } |
| 116 | +}</pre></div> |
| 117 | + |
| 118 | +<h2>π± SwiftUI Integration</h2> |
| 119 | +<p>Use AppIntentsUI to customize Siri responses with SwiftUI:</p> |
| 120 | +<div class="code-block"><div class="code-header"><span class="code-filename">AddTodoIntent+UI.swift</span></div> |
| 121 | +<pre class="code-body"><span class="kw">import</span> <span class="type">AppIntentsUI</span> |
| 122 | + |
| 123 | +<span class="kw">extension</span> <span class="type">AddTodoIntent</span> { |
| 124 | + @<span class="type">MainActor</span> |
| 125 | + <span class="kw">func</span> <span class="func">perform</span>() <span class="kw">async throws</span> -> <span class="kw">some</span> <span class="type">IntentResult</span> & <span class="type">ProvidesDialog</span> & <span class="type">ShowsSnippetView</span> { |
| 126 | + <span class="kw">let</span> todo = <span class="type">TodoStore</span>.shared.add(title: todoTitle) |
| 127 | + |
| 128 | + <span class="kw">return</span> .result( |
| 129 | + dialog: <span class="str">"\(todoTitle) μΆκ°νμ΄μ!"</span>, |
| 130 | + view: <span class="type">TodoSnippetView</span>(todo: todo) |
| 131 | + ) |
| 132 | + } |
| 133 | +} |
| 134 | + |
| 135 | +<span class="kw">struct</span> <span class="type">TodoSnippetView</span>: <span class="type">View</span> { |
| 136 | + <span class="kw">let</span> <span class="prop">todo</span>: <span class="type">TodoEntity</span> |
| 137 | + |
| 138 | + <span class="kw">var</span> <span class="prop">body</span>: <span class="kw">some</span> <span class="type">View</span> { |
| 139 | + <span class="type">HStack</span> { |
| 140 | + <span class="type">Image</span>(systemName: <span class="str">"checkmark.circle"</span>) |
| 141 | + <span class="type">Text</span>(todo.title) |
| 142 | + } |
| 143 | + .padding() |
| 144 | + } |
| 145 | +}</pre></div> |
| 146 | + |
| 147 | +<div class="tip-box"> |
| 148 | +<h4>π‘ Testing λ°©λ²</h4> |
| 149 | +<p>Run in Xcode and ask Siri "Add a todo [app name]." In the simulator, test via Hardware β Siri.</p> |
| 150 | +</div> |
| 151 | + |
| 152 | +<h2>π¦ Learning Resources</h2> |
| 153 | +<div class="tip-box" style="border:none;background:transparent;padding:0"> |
| 154 | +<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:12px"> |
| 155 | +<a href="https://m1zz.github.io/HIGLab/tutorials/appintents/documentation/higappintents/" style="background:#fff;border:1px solid #d2d2d7;border-radius:12px;padding:20px;text-decoration:none;color:inherit;text-align:center"> |
| 156 | +<div style="font-size:28px;margin-bottom:8px">π</div> |
| 157 | +<div style="font-size:13px;font-weight:600;color:#0071e3">DocC Tutorial</div> |
| 158 | +<div style="font-size:12px;color:#86868b;margin-top:2px">Try it in Xcode</div> |
| 159 | +</a> |
| 160 | +<a href="https://github.com/M1zz/HIGLab" style="background:#fff;border:1px solid #d2d2d7;border-radius:12px;padding:20px;text-decoration:none;color:inherit;text-align:center"> |
| 161 | +<div style="font-size:28px;margin-bottom:8px">π»</div> |
| 162 | +<div style="font-size:13px;font-weight:600;color:#0071e3">GitHub Project</div> |
| 163 | +<div style="font-size:12px;color:#86868b;margin-top:2px">Complete Source Code</div> |
| 164 | +</a> |
| 165 | +<a href="https://developer.apple.com/design/human-interface-guidelines/app-intents" style="background:#fff;border:1px solid #d2d2d7;border-radius:12px;padding:20px;text-decoration:none;color:inherit;text-align:center"> |
| 166 | +<div style="font-size:28px;margin-bottom:8px">π</div> |
| 167 | +<div style="font-size:13px;font-weight:600;color:#0071e3">Apple HIG Docs</div> |
| 168 | +<div style="font-size:12px;color:#86868b;margin-top:2px">App Intents κ°μ΄λ</div> |
| 169 | +</a> |
| 170 | +</div> |
| 171 | +</div> |
| 172 | + |
| 173 | +<div style="margin-top:48px;padding:24px;background:linear-gradient(135deg,#f5f5f7,#e8e8ed);border-radius:16px;"> |
| 174 | + <h2 style="font-size:20px;margin-bottom:12px;">π Apple Official Resources</h2> |
| 175 | + <div style="display:flex;flex-wrap:wrap;gap:12px;"> |
| 176 | + <a href="https://developer.apple.com/documentation/appintents" target="_blank" style="display:inline-flex;align-items:center;gap:6px;padding:10px 16px;background:white;border-radius:10px;text-decoration:none;color:var(--accent);font-weight:600;font-size:14px;border:1px solid var(--border);transition:all .2s;">π Documentation</a> |
| 177 | + <a href="https://developer.apple.com/documentation/appintents/accelerating-app-interactions-with-app-intents" target="_blank" style="display:inline-flex;align-items:center;gap:6px;padding:10px 16px;background:white;border-radius:10px;text-decoration:none;color:var(--accent);font-weight:600;font-size:14px;border:1px solid var(--border);transition:all .2s;">π» Sample Code</a> |
| 178 | + <a href="https://developer.apple.com/videos/all-videos/?q=App+Intents" target="_blank" style="display:inline-flex;align-items:center;gap:6px;padding:10px 16px;background:white;border-radius:10px;text-decoration:none;color:var(--accent);font-weight:600;font-size:14px;border:1px solid var(--border);transition:all .2s;">π¬ WWDC Sessions</a> |
| 179 | + </div> |
| 180 | +</div> |
| 181 | +</article> |
| 182 | +</body></html> |
0 commit comments