๐Ÿ„โ€โ™‚๏ธ [Flutter ร— Swift] Live Activity(WidgetKit) ์—ฐ๋™ ๋ฐ ์‚ฌ์šฉํ•ด ๋ณด๊ธฐ

Tygerยท2025๋…„ 5์›” 31์ผ
3

Flutter

๋ชฉ๋ก ๋ณด๊ธฐ
66/66
post-thumbnail

๐Ÿ„โ€โ™‚๏ธ Live Activity(WidgetKit) ์—ฐ๋™ ๋ฐ ์‚ฌ์šฉํ•ด ๋ณด๊ธฐ

ActivityConfiguration | Apple Developer
Live Activities | Apple Developer

๐ŸŽ WidgetKit ์ด๋ž€ ?
๐Ÿ”’ [Swift] ์ž ๊ธˆํ™”๋ฉด ์œ„์ ฏ ๋งŒ๋“ค๊ธฐ - WidgetKit
๐ŸŽธ [Swift] Live Activity ์‚ฌ์šฉํ•ด ๋ณด๊ธฐ

์ด๋ฒˆ ๊ธ€์—์„œ๋Š” ์•ž์„œ ์ž‘์„ฑํ•œ iOS์˜ WidgetKit์˜ ๊ธฐ๋Šฅ์ธ ํ™ˆ ์œ„์ ฏ๊ณผ Live Activitiy ๊ธฐ๋Šฅ ์ค‘ Live Activity๋ฅผ Flutter ํ”„๋กœ์ ํŠธ์— ์ถ”๊ฐ€ํ•˜๊ณ  ์—ฐ๊ฒฐํ•˜๋Š” ๋ฐฉ๋ฒ• ๋ฐ ์‚ฌ์šฉ๋ฒ•์— ๋Œ€ํ•ด์„œ ์ž‘์„ฑํ•ด๋ณด๋ ค๊ณ  ํ•œ๋‹ค.

WidgetKit ์ต์Šคํ…์…˜์„ ์ถ”๊ฐ€ํ•˜์—ฌ Flutter ํ”„๋กœ์ ํŠธ์— ์ ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•๊ณผ Live Activity ๊ธฐ๋Šฅ์„ Flutter์—์„œ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ• ๋ฐ Live Activity์˜ ์ƒํƒœ๋ฅผ ๋กœ์ปฌ ๋ฐ ์„œ๋ฒ„์—์„œ ๋ณ€๊ฒฝํ•˜๋Š” ๋ฐฉ๋ฒ•๊นŒ์ง€ ์‚ดํŽด๋ณผ ์˜ˆ์ •์ด๋‹ค.
์ถ”๊ฐ€๋กœ Flavors(๋นŒ๋“œ๋ณ€ํ˜•) ํ™˜๊ฒฝ์—์„œ iOS์˜ Extension์„ ๋‹ค๋ฃจ๋Š” ๋ฐฉ๋ฒ•๊นŒ์ง€๋„ ํ•จ๊ป˜ ์‚ดํŽด๋ณด๋„๋ก ํ•˜๊ฒ ๋‹ค.

๐Ÿ“Œ ์—ฌ๊ธฐ์„œ๋Š” WidgetKit Extension์„ Flutter์— ์ ์šฉํ•˜๊ณ  Live Activity๋ฅผ ๋‹ค๋ฃจ๋Š” ๋ฐฉ๋ฒ•๋งŒ ์ž‘์„ฑํ•˜์˜€๊ณ , ํ™ˆ & ์ž ๊ธˆํ™”๋ฉด Widget์˜ ์‚ฌ์šฉ ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด์„œ๋Š” ํ•ด๋‹น ๊ธ€์„ ์ฐธ๊ณ  !
- [Swift+WidgetKit] ํ™ˆ(Home) ์œ„์ ฏ
- [Swift+WidgetKit] ์ž ๊ธˆํ™”๋ฉด(Lock Screen) ์œ„์ ฏ

WidgetKit ? Live Activity ?

WidgetKit? Live Activity? ์ด๊ฒŒ ๋‹ค ๋ญ˜๊นŒ ?

Widget Live Activity
Home Lock Screen Lock Screen DynamicIslands DynamicIslands

์œ„์˜ ์ด๋ฏธ์ง€๊ฐ€ ๋ชจ๋‘ WidgetKit์˜ ๊ธฐ๋Šฅ๋“ค์ด๊ณ , ๋Œ€ํ‘œ์ ์œผ๋กœ ํ™ˆ๊ณผ ์ž ๊ธˆํ™”๋ฉด์— ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ๋Š” Widget๊ณผ ๋‹ค์ด๋‚˜๋ฏน์•„์ผ๋žœ๋“œ์™€ ์ž ๊ธˆํ™”๋ฉด์— ์‹ค์‹œ๊ฐ„ ํ˜„ํ™ฉ์ด๋ผ๊ณ  ๋‚˜ํƒ€๋‚˜๋Š” Live Activity๋กœ ๊ตฌ์„ฑ๋œ๋‹ค.

WidgetKit๊ณผ Live Activity์— ๋Œ€ํ•ด์„œ ๊ฐ„๋‹จํ•˜๊ฒŒ ์„ค๋ช…ํ•˜์ž๋ฉด, WidgetKit์€ iOS 14 ๋ฒ„์ „๋ถ€ํ„ฐ ๋„์ž…๋œ ํ”„๋ ˆ์ž„์›Œํฌ๋กœ, ์‚ฌ์šฉ์ž๊ฐ€ ํ™ˆ ํ™”๋ฉด ๋ฐ ์ž ๊ธˆํ™”๋ฉด์—์„œ ์†์‰ฝ๊ฒŒ ์ •๋ณด๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋Š” ์œ„์ ฏ์„ ๊ฐœ๋ฐœํ•˜๋Š”๋ฐ ์‚ฌ์šฉ๋˜๋Š” ํ™•์žฅ ๊ธฐ๋Šฅ์ด๊ณ , Live Activity๋Š” WidgetKit์— ๋‚ด์žฅ๋œ ๊ธฐ๋Šฅ ์ค‘ ํ•˜๋‚˜๋กœ, iOS 16 ๋ฒ„์ „์— ์ถ”๊ฐ€๋œ ๊ธฐ๋Šฅ์ด๊ณ  ์‚ฌ์šฉ์ž๊ฐ€ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ง„ํ–‰ ์ค‘์ธ ์ž‘์—…์„ ํ™ˆ ํ™”๋ฉด ๋ฐ ์ž ๊ธˆ ํ™”๋ฉด์—์„œ ์ง์ ‘ ์ถ”์ ํ•  ์ˆ˜ ์žˆ๋„๋ก ์ง€์›ํ•˜์—ฌ ์‚ฌ์šฉ์ž์—๊ฒŒ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์—…๋ฐ์ดํŠธ๋˜๋Š” ์ •๋ณด๋ฅผ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•œ๋‹ค. 16.2 ๋ฒ„์ „ ๋ถ€ํ„ฐ๋Š” DynamicIsland๋ฅผ ์ง€์›ํ•˜๋Š” ๋””๋ฐ”์ด์Šค์— ๋Œ€ํ•ด์„œ ์ถ”๊ฐ€์ ์ธ ์‹ค์‹œ๊ฐ„ ์ •๋ณด๋„ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค€๋‹ค.

์ฆ‰, WidgetKit์€ ์‚ฌ์šฉ์ž๊ฐ€ ์•ฑ์„ ์ง„์ž…ํ•˜์ง€ ์•Š๋”๋ผ๋„ ํ™ˆ ๋˜๋Š” ์ž ๊ธˆํ™”๋ฉด์—์„œ ์ •๋ณด๋ฅผ ํ™•์ธํ•˜๊ณ  ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ถ”์ ํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์ด๋‹ค.

WidgetKit๊ณผ Live Activity์— ๋Œ€ํ•ด์„œ ๋” ์ž์„ธํžˆ ์•Œ๊ณ  ์‹ถ๋‹ค๋ฉด ์•„๋ž˜ ๊ณต์œ ๋œ ๊ธ€์„ ์ฐธ๊ณ ํ•˜์‹œ๊ธธ ๋ฐ”๋ž€๋‹ค.

๐ŸŽ WidgetKit ์ด๋ž€ ?
๐ŸŽธ [Swift] Live Activity ์‚ฌ์šฉํ•ด ๋ณด๊ธฐ

Flutter + WidgetKit

๋ณธ๊ฒฉ์ ์œผ๋กœ Live Activity๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์•ž์„œ Flutter์— ๋ฐฐํฌ๋œ ํŒจํ‚ค์ง€ ์ค‘ Live Activity ๊ธฐ๋Šฅ์„ Flutter์—์„œ ๊ฐ„๋‹จํ•˜๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋Š” live_activities ํŒจํ‚ค์ง€๊ฐ€ ์žˆ๋‹ค.
ํ•ด๋‹น ํŒจํ‚ค์ง€๋ฅผ ์‚ฌ์šฉํ•˜๊ฒŒ ๋˜๋ฉด, Native๋กœ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•˜๋Š” ์ด๋ฒคํŠธ ๋ฐ ๊ธฐ๋Šฅ๋“ค์„ ์†์‰ฝ๊ฒŒ Flutter์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ด์ฃผ๋Š” ์ ์€ ํŽธ๋ฆฌํ•˜์ง€๋งŒ ์—ฌ์ „ํžˆ Extension์ด๋‚˜ UI ๋ถ€๋ถ„์€ ์ง์ ‘ ๋„ค์ดํ‹ฐ๋ธŒ์—์„œ ์ฒ˜๋ฆฌ๋ฅผ ํ•ด์•ผํ•˜๊ธฐ ๋–„๋ฌธ์— ์ง์ ‘ ๊ตฌ์ถ•ํ•˜๋Š” ๊ฒƒ์„ ์ถ”์ฒœํ•ฉ๋‹ˆ๋‹ค !

์•ž์„œ ์„ค๋ช…ํ•œ ๊ฒƒ๊ณผ ๊ฐ™์ด Widget์ด๋‚˜ Live Activity, DynamicIsland๋‚˜ ๋ชจ๋‘ UI๋Š” SwiftUI๋กœ ์ž‘์—…์„ ํ•ด์•ผํ•œ๋‹ค. ๊ฐ€๋ฒผ์šด UI๋งŒ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒƒ์€ SwiftUI๋ฅผ ๋ชจ๋ฅด๋”๋ผ๋„ ์กฐ๊ธˆ๋งŒ ์ฐพ์•„๋ณด๋ฉด์„œ ํ•˜๋ฉด ์–ด๋ ต์ง€๋Š” ์•Š์„ ๊ฒƒ์ด๋ผ ์ƒ๊ฐํ•œ๋‹ค.

์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ Flutter ํ”„๋กœ์ ํŠธ์— ์ ์šฉํ•ด๋ณด๋„๋ก ํ•˜์ž.

ํŒจํ‚ค์ง€ ์˜ˆ์‹œ๋กœ ๋ณด๋ฉด ์ถ•๊ตฌ ๊ฒฝ๊ธฐ ์Šค์ฝ”์–ด๊ฐ€ ๋‚˜์˜ค๋Š” ์‹ค์‹œ๊ฐ„ ํ˜„ํ™ฉ์„ ์ œ๊ณตํ•˜๊ณ  ์žˆ๋Š”๋ฐ, ์—ฌ๊ธฐ์„œ๋Š” ์˜ˆ์‹œ๋กœ ์•ผ๊ตฌ ๊ฒฝ๊ธฐ ์ ์ˆ˜๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ๊ธฐ๋Šฅ์„ ๋งŒ๋“ค์–ด๋ณด๋ฉด์„œ Live Activity๋ฅผ ์‚ฌ์šฉํ•ด ๋ณด๋„๋ก ํ•˜์ž.

Open in XCode

ํ”„๋กœ์ ํŠธ ์ˆ˜์ค€์—์„œ ํ„ฐ๋ฏธ๋„์„ ์—ด๊ณ , XCode๋ฅผ ์—ด์–ด์ฃผ๋„๋ก ํ•˜์ž. ๋งŒ์ผ .xcworkspaces ํŒŒ์ผ์ด ์—†๋‹ค๋ฉด ios ํด๋”๋ฅผ XCode์—์„œ ์ง์ ‘ ์—ด์–ด์ค˜๋„ ๋œ๋‹ค.

open ios/Runner.xcworkspace

Add Widget Extensions

Widget Extension์„ ์ถ”๊ฐ€ํ•˜์—ฌ WidgetKit์„ ์‚ฌ์šฉํ•ด ๋ณด์ž.

File > New> Targret

Product Name์€ ๋ณธ์ธ์ด ์‚ฌ์šฉํ•  ์œ„์ ฏํ‚ท์˜ ์ด๋ฆ„์„ ์ง€์ •ํ•˜์‹œ๋ฉด ๋˜๊ณ , Team์—๋Š” ํ˜„์žฌ ํ”„๋กœ์ ํŠธ์˜ ํŒ€๊ณผ ๋™์ผํ•œ ํŒ€์œผ๋กœ ์„ ํƒํ•˜์‹œ๊ณ  ๋‹ค์Œ์œผ๋กœ ๋„˜์–ด๊ฐ€๋„๋ก ํ•˜์ž.

์—ฌ๊ธฐ์„œ Live Activity ์‚ฌ์šฉ์„ ์›ํ•œ๋‹ค๋ฉด ๋ฐ˜๋“œ์‹œ "Include Live Activity"๊ฐ€ ์ฒดํฌ๋˜์–ด ์žˆ์–ด์•ผ ํ•œ๋‹ค.

Finish๋ฅผ ํ•˜๊ฒŒ ๋˜๋ฉด ์•„๋ž˜ ์•ˆ๋‚ด ๋ฌธ๊ตฌ๊ฐ€ ๋‚˜์˜ค๊ฒŒ ๋˜๋Š”๋ฐ(์•ˆ๋‚˜์˜ค๋Š” ๊ฒฝ์šฐ๋„ ์žˆ์Œ), ์ƒ์„ฑํ•˜๋ ค๋Š” ์œ„์ ฏ์˜ Scheme๊ฐ€ ํ™œ์„ฑํ™” ๋˜์ง€ ์•Š์•˜๋‹ค๊ณ  ์–ด๋–ป๊ฒŒ ํ• ๊ฑฐ๋ƒ ๋ฌป๋Š”๊ฑด๋ฐ, ์‹คํ–‰ ํƒ€๊ฒŸ์„ ์„ค์ • ํ• ์ง€์— ๋”ฐ๋ผ ์„ ํƒ์„ ํ•˜๋ฉด ๋œ๋‹ค.

์ผ๋ฐ˜์ ์œผ๋กœ ์œ„์ ฏํ‚ท์€ ์‹คํ–‰ ํƒ€๊ฒŸ์ด ์•„๋‹ˆ๋ผ ๋ฒˆ๋“ค๋กœ ํ•จ๊ป˜ ๋นŒ๋“œ๋˜๋Š” ๋ถ€์† ํƒ€๊ฒŸ์ด๊ธฐ ๋•Œ๋ฌธ์—, Don't Activate๋ฅผ ์„ ํƒํ•ด์„œ ํ™œ์„ฑํ™”ํ•˜์ง€ ์•Š์œผ๋ฉด ๋œ๋‹ค. ๋งŒ์ผ Widget ์ต์Šคํ…์…˜๋งŒ ๋ณ„๋„๋กœ ๋””๋ฒ„๊น… ๋˜๋Š” ์‹คํ–‰์ด ํ•„์š”ํ•˜๋‹ค๋ฉด ํ™œ์„ฑํ™” ํ•ด์ฃผ์‹œ๋ฉด ๋˜๊ณ , ์ด ์„ค์ •์€ ๋‚˜์ค‘์— ์–ธ์ œ๋“  ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋‹ค.

์ด์ œ Widget ์ต์Šคํ…์…˜์ด ์ƒˆ๋กญ๊ฒŒ ํ”„๋กœ์ ํŠธ์— ์ถ”๊ฐ€๋œ ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค !

์ƒˆ๋กญ๊ฒŒ ์ƒ์„ฑํ•œ Widget ์ต์Šคํ…์…˜์€ iOS ์ž…์žฅ์—์„œ๋Š” ํ•˜๋‚˜์˜ ๋…๋ฆฝ์ ์ธ ์‹คํ–‰ ๋‹จ์œ„์ด๊ธฐ ๋•Œ๋ฌธ์—, Assets, Info๋„ ๋…๋ฆฝ์ ์œผ๋กœ ์ž์‹ ์˜ ๊ฒƒ์„ ๊ฐ€์งˆ ์ˆ˜ ์žˆ๊ฒŒ ๋˜๊ณ  ๋ณ„๋„ ์„ค์ •๋“ค๋„ ๊ฐœ๋ณ„์ ์œผ๋กœ ํ•  ์ˆ˜ ์žˆ๊ฒŒ๋œ๋‹ค.

์ตœ์†Œ ์ง€์› ๋ฒ„์ „์„ ์„ค์ •ํ•ด์•ผ ํ•˜๋Š”๋ฐ, ํ˜„์žฌ ํ”„๋กœ์ ํŠธ์˜ ์ตœ์†Œ ํƒ€๊ฒŸ์œผ๋กœ ๋งž์ถฐ ์ฃผ๋ฉด๋œ๋‹ค.
๊ผญ ํ”„๋กœ์ ํŠธ์™€ ๋งž์ถฐ์•ผ ํ•˜๋Š”๊ฑด ์•„๋‹ˆ์ง€๋งŒ ๋Œ€๋ถ€๋ถ„ ๋™์ผ ํ™˜๊ฒฝ์„ ์œ ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ํ”„๋กœ์ ํŠธ์™€ ์ต์Šคํ…์…˜์˜ ์ตœ์†Œ ํƒ€๊ฒŸ์€ ๋™์ผํ•˜๊ฒŒ ๊ฐ€์ ธ๊ฐ€๋Š” ํŽธ์ด๋‹ค.

์ด์–ด์„œ LiveActivity ์‚ฌ์šฉ์„ ์œ„ํ•œ Info.plist ํŒŒ์ผ์— ํ‚ค๋ฅผ ์ถ”๊ฐ€ํ•˜๋„๋ก ํ•˜์ž.

Info.plist ํŒŒ์ผ์— "Supports Live Activities" ํ‚ค๋ฅผ YES๋กœ ์ถ”๊ฐ€ํ•ด์ฃผ๋ฉด ๋œ๋‹ค. ํ•ด๋‹น ํ‚ค ๋“ฑ๋ก์ด ์•ˆ๋˜์žˆ์œผ๋ฉด, ์•„๋ฌด๋ฆฌ Live Activity ์‹คํ–‰์„ ํ•˜๋”๋ผ๋„ ์‹œ์Šคํ…œ์—์„œ ํ™œ์„ฑํ™”๊ฐ€ ๋˜์ง€ ์•Š์œผ๋‹ˆ ๊ผญ ์žŠ์ง€๋ง๊ณ  ์ถ”๊ฐ€ํ•ด ์ฃผ์–ด์•ผ ํ•œ๋‹ค.

ํด๋” ๊ตฌ์กฐ๋ฅผ ๋ณด๋ฉด ScoreWidget, ScoreWidgetBundle, ScoreWidgetLiveActivity ์ด๋ ‡๊ฒŒ 3๊ฐœ์˜ swift ํŒŒ์ผ์ด ๊ธฐ๋ณธ์ ์œผ๋กœ ์ƒ์„ฑ๋œ๋‹ค.

์—ฌ๊ธฐ์„œ Widget ํŒŒ์ผ์€ ํ™ˆ & ์ž ๊ธˆํ™”๋ฉด ์œ„์ ฏ์— ์‚ฌ์šฉ๋˜๋Š” ํŒŒ์ผ์ด๊ธฐ ๋•Œ๋ฌธ์—, ์—ฌ๊ธฐ์„œ๋Š” ์‚ฌ์šฉํ•  ์ผ์ด ์—†์–ด ์ฃผ์„ ์ฒ˜๋ฆฌํ•˜๋„๋ก ํ•˜๊ฒ ๋‹ค.

Bundle ํŒŒ์ผ๋กœ ์™€์„œ Widget ํŒŒ์ผ์„ ์ œ๊ฑฐํ•˜๊ณ  LiveActivity์˜ ์ตœ์†Œ ์ง€์› ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ด ์ฃผ๋„๋ก ํ•˜์ž.

import WidgetKit
import SwiftUI

@main
struct ScoreWidgetBundle: WidgetBundle {
    var body: some Widget {
//        ScoreWidget()
        if #available(iOS 16.1, *) {
            ScoreWidgetLiveActivity()
        }
    }
}

LiveActivity ํŒŒ์ผ์€ ์•„๋ž˜์—์„œ ๋” ์ž์„ธํžˆ ์‚ดํŽด๋ณผ ์˜ˆ์ •์ด๋‹ˆ ์šฐ์„ ์€ ์—ฌ๊ธฐ๊นŒ์ง€ ํ•˜๊ณ  ์‹คํ–‰์„ ํ•ด๋ณด์ž !

์ž ๋ชจ๋‘ ์‹คํ–‰์ด ์ž˜ ๋˜์—ˆ๋Š”๊ฐ€ ? ์•„๋งˆ ๋Œ€๋ถ€๋ถ„ ์‹คํ–‰์— ์‹คํŒจํ•˜์˜€์„ ๊ฒƒ์ด๋‹ค.

์ฒœ์ฒœํžˆ ์—๋Ÿฌ๋ฅผ ํ•ด๊ฒฐํ•ด ๋ณด๋„๋กํ•˜์ž. ๋Œ€๋ถ€๋ถ„์˜ Flutter์—์„œ ์ต์Šคํ…์…˜ ์ถ”๊ฐ€์‹œ ๋ฐœ์ƒํ•˜๋Š” ์ผ€์ด์Šค์™€ Flavors ํ™˜๊ฒฝ์— ๋”ฐ๋ฅธ ์ผ€์ด์Šค๋“ค์ธ๋ฐ, ์ด ์™ธ ์ผ€์ด์Šค๋“ค์€ ๋Œ“๊ธ€์ด๋‚˜ ๋ฉ”์ผ ์ฃผ์‹œ๋ฉด ์ตœ๋Œ€ํ•œ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋„๋ก ๋„์™€ ๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค..


Case 1. PreviewActivityBuilder

ํ•ด๋‹น ์—๋Ÿฌ๋Š” ๊ธฐ๋ณธ์œผ๋กœ ์ถ”๊ฐ€๋œ Preview์˜ ์ฝ”๋“œ๊ฐ€ ํŠน์ • ๋ฒ„์ „ ์ด์ƒ์—์„œ๋งŒ ์ง€์›๋˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ๋ฏธ๋ฆฌ๋ณด๊ธฐ(as: .content, contentState: ..)๋Š” iOS 17.2 ์ด์ƒ์—์„œ๋งŒ ์ง€์›๋˜๊ธฐ ๋•Œ๋ฌธ์— Result Builder ์‹œ์Šคํ…œ์ด ํ•ด๋‹น ๋ธ”๋ก์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” API ๋ฒ„์ „์„ ๋งŒ์กฑํ•˜์ง€ ๋ชปํ•ด ์ปดํŒŒ์ผ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•œ ๊ฒƒ์ด๋‹ค.

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์œผ๋กœ๋Š” LiveActivity ํŒŒ์ผ ์•„๋ž˜ ํ”„๋ฆฌ๋ทฐ๋ฅผ ์ฃผ์„์ฒ˜๋ฆฌํ•˜๊ฑฐ๋‚˜ ๋˜๋Š” ์ƒ๋‹จ์— ๋ฒ„์ „์„ ๋ช…์‹œํ•ด์ฃผ๋ฉด ๋œ๋‹ค.


Case 2. Cycle inside Runner

Swift ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ๋ฐœ์ƒํ•˜์ง€ ์•Š์ง€๋งŒ, ์ด์ƒํ•˜๊ฒŒ Flutter ํ”„๋กœ์ ํŠธ์—์„œ ์ต์Šคํ…์…˜์„ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒฝ์šฐ์—๋งŒ ์ฃผ๋กœ ๋ฐœ์ƒํ•˜๋Š” ์—๋Ÿฌ๋กœ, XCode์—์„œ ๋นŒ๋“œ ํƒ€๊ฒŸ ๊ฐ„ ์ˆœํ™˜ ์˜์กด์„ฑ(Circular Dependency) ๋ฌธ์ œ๊ฐ€ ์ƒ๊ฒผ์„ ๋•Œ ๋ฐœ์ƒํ•˜๊ฒŒ ๋˜๋Š” ์—๋Ÿฌ์ด๋‹ค.

Build Phases์˜ ์‹คํ–‰ ์ˆœ์„œ์— ๋”ฐ๋ผ, ์˜์กด์„ฑ ํƒ€๊ฒŸ์ด ๊ผฌ์ด๋ฉด์„œ ์ˆœํ™˜ ์˜์กด์„ฑ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์— ์ˆœ์„œ๋ฅผ ์žฌ๋ฐฐ์น˜ ํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

์ œ๊ฐ€ ์ฐพ์€ ๋ฐฉ๋ฒ•์œผ๋กœ๋Š” "Embed Foundation Extensions"๊ฐ€ "Thin Binary"๋ณด๋‹ค ์„ ํ–‰๋˜์–ด์•ผ ํ•œ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค. ํ™•์žฅ์„ ๋จผ์ € ์ฒ˜๋ฆฌํ•˜๊ณ  ๋ฉ”์ธ ๋ฐ”์ด๋„ˆ๋ฆฌ์— ๋ณ‘ํ•ฉ๋˜์–ด์•ผ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๋Š”๋‹ค.

๋‹จ ๋‹ค๋ฅธ ์ˆœ์„œ๋กœ ๊ผฌ์ผ ์ˆ˜๋„ ์žˆ์œผ๋‹ˆ Build phases ์ˆœ์„œ ์žฌ๋ฐฐ์น˜ํ•˜์‹œ๋ฉด ๋Œ€๋ถ€๋ถ„ ํ•ด๊ฒฐ์ด ๊ฐ€๋Šฅํ•˜๋‹ค.


Case 3. Bundle Identifier

๋งˆ์ง€๋ง‰ ์ผ€์ด์Šค๋Š” ๋ฐ”๋กœ Bundle Identifier ๋ฌธ์ œ์ด๋‹ค. Flavors๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ๋‹จ์ผ ํ™˜๊ฒฝ๋งŒ ๊ตฌ์ถ•ํ•œ ๊ฒฝ์šฐ์—๋Š” ๋ฐœ์ƒํ•˜์ง€ ์•Š๊ณ  ๋นŒ๋“œ ๋ณ€ํ˜•์œผ๋กœ ์ธํ•ด์„œ ๋ฐœ์ƒํ•˜๋Š” ์—๋Ÿฌ ์ผ€์ด์Šค๋‹ค.

์ต์Šคํ…์…˜์€ ๋ถ€๋ชจ Idnetifier์— ๊ณ ์ •๋˜๊ฒŒ ๋˜๋Š”๋ฐ, ๋ถ€๋ชจ Runner๊ฐ€ ๋ณ€ํ˜•๋œ ์ƒํƒœ์ด๋ฏ€๋กœ ์ต์Šคํ…์…˜๋„ ๋™์ผํ•˜๊ฒŒ ๋นŒ๋“œ๋ณ€ํ˜•์„ ์ ์šฉํ•ด์ฃผ๋ฉด ํ•ด๊ฒฐ๋œ๋‹ค.

๋งŒ์ผ ๋นŒ๋“œ๋ณ€ํ˜•์ด ๋˜์–ด์žˆ๋Š”๋ฐ, ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š์•˜๋‹ค๋ฉด ์•„๋งˆ๋„ production ์•ฑ์œผ๋กœ ์‹คํ–‰ํ•œ ๊ฒฝ์šฐ์ผ ๊ฒƒ์ด๋‹ค.
production์—๋Š” suffix๋ฅผ ๋ถ™์ด์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์—, suffix๊ฐ€ ๋ถ™๋Š” (dev, qa, stg...) ์•ฑ์œผ๋กœ ์‹คํ–‰ํ•ด๋ณด๋ฉด ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.

์ต์Šคํ…์…˜์„ ํ™•์ธํ•ด๋ณด๋ฉด ๋ถ€๋ชจ Bundle Identifier์— ์ƒ์„ฑํ•œ ์ต์Šคํ…์…˜๋ช…์œผ๋กœ ๋…๋ฆฝ๋œ Bundle Identifier๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ์œ„์ ฏ์ด ์ƒ์„ฑ๋œ ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

Flavors ์ ์šฉ์‹œ ๋ฉ”์ธ ํƒ€๊ฒŸ์— ํ•œ ๊ฒƒ๊ณผ ๋™์ผํ•˜๊ฒŒ ์ต์Šคํ…์…˜์—๋„ suffix๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ๋นŒ๋“œ๋ณ€ํ˜•์— ๋”ฐ๋ฅธ ๋ถ€๋ชจ Bundle Identifier์™€ ์‹ฑํฌ๊ฐ€ ๋งž๋„๋ก ํ•ด์ฃผ์ž.

๋นŒ๋“œ ๋ณ€ํ˜•์ด ์ด๋ฏธ ์„ธํŒ…๋œ ํ”„๋กœ์ ํŠธ์ธ ๊ฒฝ์šฐ์— ๊ฐœ๋…์ด ์–ด๋ ค์šฐ์‹œ๋‹ค๋ฉด ์•„๋ž˜ ๊ณต์œ ๋œ ๊ธ€์„ ์ฐธ๊ณ ํ•˜์‹œ๊ธธ ๋ฐ”๋ž€๋‹ค.
[Flutter] Flavor ๋นŒ๋“œ ๋ณ€ํ˜•


โ—๏ธ Xcode <Flutter/Flutter.h> file not found, after adding Extension to project

์ถ”๊ฐ€๋กœ ์ต์Šคํ…์…˜์œผ๋กœ ์ธํ•˜์—ฌ Flutter/Flutter.h ํŒŒ์ผ์„ ์ฐพ์ง€ ๋ชปํ•œ๋‹ค๋Š” ์—๋Ÿฌ๋‚˜ Runner-Bridging-Header.h๊ฐ€ Flutter ํ—ค๋”๋ฅผ ์ฐธ์กฐํ•˜๋Š”๋ฐ ๊ฒฝ๋กœ๊ฐ€ ๋งž์ง€ ์•Š๋Š”๋‹ค๋Š” ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค๋ฉด, Other Linker Flags์™€ Objective-C Bridging Header ๊ฒฝ๋กœ๋ฅผ ์—†์• ์ฃผ๋ฉด ๋œ๋‹ค.

iOS ์ต์Šคํ…์…˜์€ Flutter ์—”์ง„์„ ๋กœ๋“œํ•˜์ง€ ๋ชปํ•˜๊ณ , ๋ฉ”์ธ ์•ฑ(Runner)๊ณผ ๋ณ„๋„์˜ ํƒ€๊ฒŸ์œผ๋กœ ์‹คํ–‰๋˜๊ธฐ ๋•Œ๋ฌธ์—, Flutter ๊ด€๋ จ ์ฐธ์กฐ๋ฅผ ๋ชจ๋‘ ์ œ๊ฑฐํ•˜์—ฌ์•ผ ํ•œ๋‹ค. ์ด๊ฑด Widget ์ต์Šคํ…์…˜ ์™ธ์—๋„ ๋ชจ๋“  ์ต์Šคํ…์…˜์— ํ•ด๋‹น๋˜๋Š” ๋‚ด์šฉ์ด๋‹ˆ ๊ฐ™์€ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค๋ฉด ๋™์ผํ•˜๊ฒŒ ํ•ด๊ฒฐํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

XCode์—์„œ ํ•ด๋‹น ์„ค์ •์œผ๋กœ ์ด๋™ํ•˜์—ฌ ์ฐธ์กฐ๋˜์–ด ์žˆ๋‹ค๋ฉด ์ „๋ถ€ ์ œ๊ฑฐํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

๐Ÿ”ฅ Widget์˜ ์ต์Šคํ…์…˜ ํƒ€๊ฒŸ์—์„œ ์ œ๊ฑฐํ•ด์•ผ์ง€ Runner ํƒ€๊ฒŸ์—์„œ ์ œ๊ฑฐํ•˜๋ฉด ์•ˆ๋จ !!!!

Other Linker Flags Objective-C Bridging Header

์ž ์ด์ œ ๋ฌธ์ œ ์—†์ด ์•ฑ์ด ์‹คํ–‰๋˜๊ณ , ํ…Œ์ŠคํŠธ๋กœ Live Activity์™€ DynamicIsland๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ํ™œ์„ฑํ™” ๋˜๋Š”์ง€๊นŒ์ง€ ํ™•์ธํ•ด๋ณด์ž.

AppDelegate.swift

์šฐ์„ ์€ ์•„๋ž˜ ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ๋จผ์ € ํ…Œ์ŠคํŠธ๋ฅผ ํ•ด๋ณด๋„๋ก ํ•˜๊ฒ ๋‹ค.

import Flutter
import UIKit
import ActivityKit

@main
@objc class AppDelegate: FlutterAppDelegate {
    override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        GeneratedPluginRegistrant.register(with: self)
        
        startLiveActivity()
        
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }
    
    
    func startLiveActivity() {
        if ActivityAuthorizationInfo().areActivitiesEnabled {
            let attributes = ScoreWidgetAttributes(name: "TYGER")
            let contentState = ScoreWidgetAttributes.ContentState(emoji: "๐Ÿ”ฅ")
            
            do {
                let _ = try Activity<ScoreWidgetAttributes>.request(
                    attributes: attributes,
                    contentState: contentState,
                    pushType: nil // or `.token` if push-to-start
                )
                print("โœ… Live Activity started.")
            } catch {
                print("โŒ Failed to start Live Activity: \(error)")
            }
        } else {
            print("โš ๏ธ Live Activities are not enabled.")
        }
    }
}

์ต์Šคํ…์…˜์˜ ๋ช…์นญ์ด ๋‹ค๋ฅด๋‹ค๋ฉด Attributes ๊ฐ์ฒด๊ฐ€ ๋‹ค๋ฅด๊ธฐ ๋•Œ๋ฌธ์— ๋ณธ์ธ์ด ์ƒ์„ฑํ•œ ์œ„์ ฏ ๋ช…์นญ์œผ๋กœ ๋ณ€๊ฒฝํ•˜์…”์•ผ ํ•œ๋‹ค.

Attributes ๊ฐ์ฒด๋ฅผ ์ฐพ์ง€ ๋ชปํ•œ๋‹ค๋Š” ์—๋Ÿฌ๊ฐ€ ๋‚˜์˜ฌํ…๋ฐ, ์ถ”๊ฐ€ํ•œ ์ต์Šคํ…์…˜์ด Runner์— ํฌํ•จ๋˜์ง€ ์•Š์•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

์œ„์ ฏ์˜ LiveActivity ํŒŒ์ผ๋กœ ์ด๋™ํ•˜์—ฌ ์šฐ์ธก์˜ File Inspector ์—ด์–ด๋ณด๋ฉด, Target Membership์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋Š”๋ฐ, Runner ํƒ€๊ฒŸ์ด ํฌํ•จ๋˜์–ด ์žˆ์ง€ ์•Š๋Š” ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ๋‹ค.

์ถ”๊ฐ€ ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ์„œ Runner ์•ฑ์„ ์„ ํƒํ•œ ํ›„ ์ €์žฅํ•ด์ฃผ๋ฉด ์ž๋™์œผ๋กœ Delegate ํŒŒ์ผ์—์„œ ๊ฐ์ฒด๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

์ด์ œ ์•ฑ์„ ์‹คํ–‰ํ•ด ๋ณด๋ฉด ์‹คํ–‰๊ณผ ๋™์‹œ์— ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ๊ฐ€ ํ™œ์„ฑํ™” ๋˜๊ณ  ๋ฐฑ๊ทธ๋ผ์šด๋“œ๋กœ ๋‚˜๊ฐ€๋ฉด ์ž๋™์œผ๋กœ ๋‹ค์ด๋‚˜๋ฏน์•„์ผ๋žœ๋“œ๊ฐ€ ํ‘œ์‹œ๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

๋งŒ์•ฝ์— ์‹คํ–‰์ด ์•ˆ๋˜์‹œ๊ฑฐ๋‚˜ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค๋ฉด ๋Œ“๊ธ€ ๋‚จ๊ฒจ์ฃผ์„ธ์š”. ํ™•์ธ ํ›„ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋„๋ก ๋„์›€ ๋“œ๋ฆฌ๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค !

Live Activity

Live Activity ์‚ฌ์šฉ์„ ์œ„ํ•œ ๊ธฐ๋ณธ์ ์ธ ์„ธํŒ…์€ ๋์ด๋‚ฌ๋‹ค. ์ด์ œ UI๋ฅผ ๋งŒ๋“ค๊ณ  ์ด๋ฒคํŠธ๋ฅผ ์—ฐ๊ฒฐํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

์ด ๊ธ€์—์„œ๋Š” UI ๋‚ด์šฉ์€ SwiftUI๋กœ ์ž‘์„ฑ๋˜๊ธฐ์— ๋ณ„๋„๋กœ ๋‹ค๋ฃจ์ง€๋Š” ์•Š์„ ๊ฒƒ์ด๊ณ , SwiftUI๋ฅผ ๊ฒฝํ—˜ํ•˜์ง€ ์•Š์•˜๋”๋ผ๋„ ๊ทธ๋ฆฌ ์–ด๋ ต์ง€๋Š” ์•Š์œผ๋‹ˆ ์กฐ๊ธˆ๋งŒ ์ฐพ์•„๋ณด๋ฉด ๋ชจ๋‘ ์›ํ•˜๋Š” UI๋ฅผ ๋งŒ๋“ค์–ด๋‚ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์„œ ์ž‘์„ฑ๋œ ์˜ˆ์ œ์˜ UI ํŒŒ์ผ๋งŒ ๋ณ„๋„๋กœ ๊ณต์œ ํ•˜๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. ํ•„์š”ํ•˜์‹œ๋ฉด ์ฐธ๊ณ ํ•˜์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

์•ผ๊ตฌ ์ ์ˆ˜๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ์‹ค์‹œ๊ฐ„ ํ˜„ํ™ฉ์„ ์ž‘์—…ํ•˜๊ธฐ์— ์•ž์„œ ์กฐ๊ธˆ๋” ๋‹จ์ˆœํ•œ ๊ตฌ์กฐ์ธ ๋ฐฐ๋‹ฌ์˜ ๋ฏผ์กฑ, ์š”๊ธฐ์š”, ์Šคํƒ€๋ฒ…์Šค ๋“ฑ๊ณผ ๊ฐ™์€ ํ”ฝ์—… ํ˜„ํ™ฉ์„ ๋ณด์—ฌ์ฃผ๋Š” ๊ธฐ๋Šฅ์œผ๋กœ ๋จผ์ € Live Activity์— ๋Œ€ํ•ด์„œ ์•Œ์•„๋ณด๋„๋ก ํ•˜์ž.

Live Activity๋ฅผ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด ์ž‘์—…ํ•ด์•ผ ํ•˜๋Š” ๊ธฐ๋Šฅ๋“ค์„ ๋จผ์ € ์ •๋ฆฌํ•ด๋ณด๋„๋ก ํ•˜์ž.

๊ฐ€์žฅ ์šฐ์„ ์‹œ ๋˜๋Š” ๊ฒƒ์€ ๋‹น์—ฐํžˆ Live Activity๋ฅผ ํ™œ์„ฑํ™”์‹œํ‚ค๋Š” ๊ฒƒ์ด๋‹ค. ์ด๋ฏธ ์œ„์—์„œ ๊ธฐ๋ณธ์ ์ธ ํ™œ์„ฑํ™” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด์„œ๋Š” ์•Œ๊ณ  ์žˆ๋‹ค.

๋‹ค์Œ์œผ๋กœ๋Š” Live Activity์˜ ๋ชฉ์ ์— ๋งž๊ฒŒ ๋ฐ์ดํ„ฐ๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์—…๋ฐ์ดํŠธ(Update)ํ•˜๋Š” ๊ฒƒ์ด๋‹ค.
์—ฌ๊ธฐ์„œ ์—…๋ฐ์ดํŠธ๋ฅผ ํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ๋‘ ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์ด ์žˆ๋‹ค. ๋ฐ”๋กœ ๋กœ์ปฌ๊ณผ ์„œ๋ฒ„์—์„œ ์—…๋ฐ์ดํŠธ ์ด๋ฒคํŠธ๋ฅผ ๋ฐœ์ƒํ•˜์—ฌ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋Š”๋ฐ, ๊ฑฐ์˜ ์„œ๋ฒ„ ์š”์ฒญ์— ์˜ํ•ด ์—…๋ฐ์ดํŠธ๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ๊ฒƒ์ด ์ผ๋ฐ˜์ ์ผ ๊ฒƒ์ด๋‹ค.
์™œ ์ฃผ๋กœ ์„œ๋ฒ„์—์„œ ์—…๋ฐ์ดํŠธ๋ฅผ ํ•˜๋Š”๊ฑธ๊นŒ ? Live Activity๋Š” ๋ชฉ์  ์ž์ฒด๊ฐ€ ์•ฑ์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š์•„๋„ ์‚ฌ์šฉ์ž์—๊ฒŒ ํ˜„ํ™ฉ์„ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ณด์—ฌ์ค€๋‹ค๋Š” ์ ์ด๊ธฐ ๋•Œ๋ฌธ์— ์•ฑ์ด ์ข…๋ฃŒ๊ฐ€ ๋˜์—ˆ๋”๋ผ๋„ Live Activity๋Š” ๊ณ„์† ๋…ธ์ถœ์ด ๊ฐ€๋Šฅํ•˜๋‹ค๋Š” ๊ฒƒ์ด๋‹ค.
๋กœ์ปฌ์—์„œ์˜ ์—…๋ฐ์ดํŠธ๋Š” ์•ฑ์ด ์ข…๋ฃŒ๋˜๋ฉด, ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•˜๋Š” ์ด๋ฒคํŠธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ฌ ์ˆ˜๊ฐ€ ์—†์–ด์„œ ๋ฐ˜๋“œ์‹œ ์„œ๋ฒ„๊ฐ€ ์š”์ฒญ์„ ํ•ด์•ผํ•œ๋‹ค.

๊ทธ๋ ‡๋‹ค๋ฉด ์„œ๋ฒ„๋Š” ์–ด๋–ป๊ฒŒ Live Activity์˜ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์ผ๊นŒ ? ๋ฐ”๋กœ ํ‘ธ์‹œ(Push Notification) ์‹œ์Šคํ…œ์„ ์ด์šฉํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

ํ‘ธ์‹œ ์•Œ๋ฆผ์—๋Š” ์‚ฌ์šฉ์ž์—๊ฒŒ ์•Œ๋ฆผ ๋ฐฐ๋„ˆ๋ฅผ ๋„์šฐ๋Š” ์—ญํ• ๋„ ์žˆ์ง€๋งŒ ๊ทธ ์™ธ์—๋„ ๋ฐ์ดํ„ฐ ์ „์†ก, ์‚ฌ์ผ๋ŸฐํŠธ, ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ์—๋„ ํ™œ์šฉํ•  ์ˆ˜๊ฐ€ ์žˆ๋‹ค.

ํ˜„์žฌ ์ž์‹ ์˜ ํ”„๋กœ์ ํŠธ์— ํ‘ธ์‹œ๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ์žˆ๋‹ค๋ฉด, ๋จผ์ € ํ‘ธ์‹œ๋ฅผ ์„ธํŒ…ํ•˜๋Š” ์ž‘์—…์ด ์„ ํ–‰๋˜์–ด์•ผ ํ•œ๋‹ค. ์ €๋Š” ํ”„๋กœ์ ํŠธ์— Firebase Messaging(FCM)์ด ์„ธํŒ…๋˜์–ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์— FCM์œผ๋กœ ์ž‘์—…ํ•  ์˜ˆ์ •์ด๊ณ , ๊ผญ FCM์ด ์•„๋‹ˆ๋”๋ผ๋„ payload๋Š” ๋™์ผํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋ฌธ์ œ๋Š” ์—†๋‹ค.

*Flutter์—์„œ Firebase Messaging ์—ฐ๋™ ๋ฐ ์‚ฌ์šฉ ๋ฐฉ๋ฒ•์€ ์•„๋ž˜ ๊ธ€ ์ฐธ๊ณ  !!

Firebase FCM(Firebase Cloud Message) ์‚ฌ์šฉํ•ด ๋ณด๊ธฐ

๋งˆ์ง€๋ง‰์œผ๋กœ ๋ชจ๋“  ํ”Œ๋กœ์šฐ๊ฐ€ ๋๋‚œ๋‹ค๋ฉด, Live Activity์˜ DynamicIsland์˜ ๋น„ํ™œ์„ฑํ™” ๋ฐ LockScreen ์ œ๊ฑฐ ๋“ฑ์˜ ์ข…๋ฃŒํ•˜๋Š” ๋ฐฉ๋ฒ•์ด๋‹ค.

์ด ํ๋ฆ„๋Œ€๋กœ ๊ธฐ๋Šฅ์„ ์‚ดํŽด๋ณด๋ฉด์„œ, ๋งŒ๋“ค์–ด๋ณด๋„๋ก ํ•˜์ž !

Design Guidelines

Live Activities | Apple Developer
๐ŸŽธ [Swift] Live Activity ์‚ฌ์šฉํ•ด ๋ณด๊ธฐ

Live Activity ๋””์ž์ธ์€ ํฌ๊ฒŒ ๋‹ค์ด๋‚˜๋ฏน์•„์ผ๋žœ๋“œ(DynamicIsland)์™€ ์ž ๊ธˆํ™”๋ฉด(LockScreen)์œผ๋กœ ๋‚˜๋‰˜๊ฒŒ ๋˜๊ณ  ๋‹ค์ด๋‚˜๋ฏน์•„์ผ๋žœ๋“œ์—๋Š” 3๊ฐ€์ง€์˜ ํƒ€์ž…์ด ์ง€์ •๋˜์–ด ์žˆ๋‹ค.

์ฐธ๊ณ ๋กœ ๋””์ž์ธ ๊ฐ€์ด๋“œ๋ผ์ธ ๊ทœ์ •์„ ์ค€์ˆ˜ํ•˜์ง€ ์•Š์œผ๋ฉด ์‹œ์Šคํ…œ์— ์˜ํ•ด ๊ฑฐ์ ˆ๋  ์ˆ˜ ์žˆ์œผ๋‹ˆ, ์• ํ”Œ์˜ ๋””์ž์ธ ๊ฐ€์ด๋“œ๋ผ์ธ์— ๋งž๋Š” UI๋ฅผ ๋งŒ๋“ค์–ด์•ผ ํ•œ๋‹ค.

๋‹ค์ด๋‚˜๋ฏน์•„์ผ๋žœ๋“œ+์ž ๊ธˆํ™”๋ฉด์€ ํ•จ๊ป˜ ํ™œ์„ฑํ™”๋˜๊ธฐ์— ๋ฐ˜๋“œ์‹œ ๋ชจ๋“  ๋””์ž์ธ๊ณผ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ด์•ผ ํ•œ๋‹ค.

DynamicIsland Lock Screen
Expand Compact Minimal

Live Activity๋Š” iOS 16.1 ๋ฒ„์ „๋ถ€ํ„ฐ ์ง€์›์ด ๊ฐ€๋Šฅํ•˜๊ณ , DynamicIsland ๊ธฐ๋Šฅ์€ iPhone 14Pro ๋ถ€ํ„ฐ ์ง€์›๋œ๋‹ค.

Live Activity๊ฐ€ ํ™œ์„ฑํ™”๋˜๋ฉด Lock Screen์—๋Š” ์ฆ‰์‹œ ๋…ธ์ถœ๋˜๊ณ , DynamicIsland๋Š” ์•ฑ์ด ๋ฐฑ๊ทธ๋ผ์šด๋“œ(background) ๋˜๋Š” ์ข…๋ฃŒ(terminated) ์ƒํƒœ๊ฐ€ ๋˜๋ฉด, ์•ฑ์ด DynamicIsland์— ๋“ค์–ด๊ฐ€๋ฉด์„œ ํ™œ์„ฑํ™”๊ฐ€ ์‹œ์ž‘๋œ๋‹ค.

Live Activity์˜ ์ง€์† ๋…ธ์ถœ ์ตœ๋Œ€ ์‹œ๊ฐ„์€ 8์‹œ๊ฐ„์ด๊ณ , ์ž ๊ธˆํ™”๋ฉด(LockScreen)์—์„œ๋Š” ์ข…๋ฃŒ ํ›„ ์ตœ๋Œ€ 4์‹œ๊ฐ„๊นŒ์ง€(์ด 12์‹œ๊ฐ„) ๋…ธ์ถœ์„ ํ•  ์ˆ˜ ์žˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ข…๋ฃŒ(end) ์ด๋ฒคํŠธ ๋ฐœ์ƒ ํ›„์—๋Š” ๋” ์ด์ƒ DynamicIsland๋Š” ํ™œ์„ฑํ™” ๋˜์ง€ ์•Š๋Š”๋‹ค.

DynamicIsland์˜ ๊ธฐ๋ณธ ํƒ€์ž…์€ Compact ์ƒํƒœ๋กœ, ์ด ๋•Œ๋Š” ๊ฐ€์šด๋ฐ๋ฅผ ์ค‘์‹ฌ์œผ๋กœ leading, trailing side๊ฐ€ ํ‘œ์‹œ๋˜๊ณ , 2๊ฐœ ์ด์ƒ์˜ ์•ฑ์ด Live Activity๋ฅผ ์š”์ฒญํ•˜๊ฒŒ ๋˜๋ฉด Minimal ์ƒํƒœ๋กœ ๋ฐ”๋€Œ๊ฒŒ ๋œ๋‹ค. ์ฃผ๋กœ Minimal ์ƒํƒœ์—์„œ๋Š” Compact์˜ leading, trailing side์˜ UI๋ฅผ ์žฌ์‚ฌ์šฉํ•œ๋‹ค. ๋ฌผ๋ก  ๋‹ค๋ฅด๊ฒŒ ๊ตฌ์„ฑํ•ด๋„ ์ƒ๊ด€์—†๋‹ค.

Expand๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ๊ธธ๊ฒŒ ๋ˆ„๋ฅด๊ณ  ์žˆ์„ ๋•Œ์— ๋…ธ์ถœ๋˜๋Š” ์ƒํƒœ๋กœ ์ด ์˜์—ญ์˜ ๋ชจ๋“  ํฌ์ง€์…˜์„ ์‚ฌ์šฉํ•˜์ง€๋Š” ์•Š์•„๋„ ๋œ๋‹ค. ์‹ค์ œ๋กœ Leading, Trailing ์—†์ด Center๋งŒ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ๋„ ์žˆ๊ณ , Leading๊ณผ Option(.bottom)๋งŒ ์‚ฌ์šฉํ•˜๋Š” ์•ฑ๋“ค๋„ ์žˆ๋‹ค.

์ตœ๋Œ€ํ•œ ์‹ค์‹œ๊ฐ„์„ฑ ์ •๋ณด๋ฅผ ๋ณด์—ฌ์ฃผ๊ธฐ ์œ„ํ•œ ๋ฐ์ดํ„ฐ์™€ UI ์ œ๊ณต์— ์ง‘์ค‘ํ•ด์•ผ ํ•˜๊ณ , ๊ณต๊ฐ„์ด๋‚˜ ํƒ€์ž…์ด ์ œํ•œ์ ์ด๋‹ˆ ๋กœ๊ณ  ๋˜๋Š” ์•ฑ์„ ๋ณด์—ฌ์ฃผ๊ธฐ ์œ„ํ•œ ๋ถˆํ•„์š”ํ•˜๊ณ  ์˜๋ฏธ์—†๋Š” UI ๊ตฌ์„ฑ์€ ์ž์ œํ•˜๋ผ๊ณ  ํ•œ๋‹ค.

Getting Started with Live Activities

์•ž์„œ ํ…Œ์ŠคํŠธ์—์„œ ์–ด๋–ป๊ฒŒ Live Activity๋ฅผ ์‹œ์ž‘ํ•˜๋Š”์ง€ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด ๋ดค๋Š”๋ฐ, ๋””ํ…Œ์ผํ•˜๊ฒŒ ์‚ดํŽด๋ณด๊ณ  ์ด๋ฒˆ์—๋Š” Swift์—์„œ ์‹œ์ž‘ํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹Œ, Platform Channel์„ ํ†ตํ•ด Flutter์—์„œ ์‹œ์ž‘ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๊ตฌ์กฐ๋ฅผ ๋งŒ๋“ค์–ด ๋ณด์ž.

๊ธฐ์กด AppDelegate.swift ํŒŒ์ผ์— ์ž‘์„ฑํ•œ ์ฝ”๋“œ๋Š” ์ „๋ถ€ ์ œ๊ฑฐํ•ด์ฃผ์ž.

LiveActivity๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ํ˜ธ์ถœํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ActivityKit์—์„œ ์ œ๊ณตํ•˜๋Š”, Activity ๊ฐ์ฒด๋ฅผ ์„ ์–ธํ•˜๊ณ  request๋ฅผ ํ˜ธ์ถœํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

import ActivityKit

static var currentActivity: Activity<SampleWidgetAttributes>?

currentActivity = try Activity<SampleWidgetAttributes>.request(
		attributes: attributes,
		contentState: state,
		pushType: nil,
)

pushType๊ณผ attributes์— ๋Œ€ํ•ด์„œ ์•Œ์•„๋ณด์ž

attributes๋Š” ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ์˜ ์ •์ ์ธ ๋ฐ์ดํ„ฐ๋ฅผ ์˜๋ฏธํ•˜๋ฉฐ, ์‹œ์ž‘๋  ๋•Œ ํ•œ๋ฒˆ ์„ค์ •๋˜๋Š” ๊ฐ’์œผ๋กœ, ์—…๋ฐ์ดํŠธ์˜ ๋Œ€์ƒ์€ ContentState๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

ContentState๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๊ธฐ ์œ„ํ•ด ์„œ๋ฒ„์—์„œ ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ ์ „์šฉ ํ‘ธ์‹œ๋ฅผ ํ†ตํ•œ ์ƒํƒœ ๋ณ€๊ฒฝ ์š”์ฒญ๊ณผ ๋กœ์ปฌ์—์„œ ActivityKit์˜ update๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์š”์ฒญํ•˜๋Š” ๋‘ ๊ฐ€์ง€ ๋ฐฉ์‹์ด ์žˆ๋Š”๋ฐ, ์ด ๋•Œ์— pushType์„ ์‚ฌ์šฉํ•˜๊ฒŒ ๋œ๋‹ค.

์ง€๊ธˆ์ฒ˜๋Ÿผ pushType์„ nil๋กœ ์ง€์ •ํ•˜๊ฒŒ ๋˜๋ฉด, ๋กœ์ปฌ์—์„œ๋งŒ ์—…๋ฐ์ดํŠธ๋ฅผ ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์ด๊ณ , ์„œ๋ฒ„์—์„œ ์—…๋ฐ์ดํŠธ๊ฐ€ ํ•„์š”ํ•˜๋‹ค๋ฉด .token์„ ์ง€์ •ํ•ด ์ฃผ๋ฉด ๋œ๋‹ค.

์ด์ œ๋ถ€ํ„ฐ๋Š” ์˜ˆ์ œ๋ฅผ ํ†ตํ•ด ๋ณธ๊ฒฉ์ ์ธ ์‚ฌ์šฉ ๋ฐฉ๋ฒ•๋“ค์„ ์•Œ์•„๋ณด์ž.
์•ž์„œ ์„ค๋ช…ํ•œ ๊ฒƒ๊ณผ ๊ฐ™์ด ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ ์ค‘ ๊ฐ€์žฅ ์‹ฌํ”Œํ•œ ๊ตฌ์กฐ์ธ ์Šคํƒ€๋ฒ…์Šค์˜ ์Œ๋ฃŒ ํ”ฝ์—… ํ˜„ํ™ฉ ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ๋ฅผ ๋งŒ๋“ค์–ด ๋ณด๋ฉด์„œ ์ข€ ๋” ์ž์„ธํžˆ ์‚ดํŽด๋ณด๋„๋ก ํ•˜์ž.

OrderWidget์ด๋ผ๋Š” ์œ„์ ฏ ์ต์Šคํ…์…˜์„ ์ถ”๊ฐ€ํ•ด ์ฃผ์—ˆ๋‹ค. Attributes์—๋Š” ์ •์  ๋ฐ์ดํ„ฐ๋กœ name, address, count ๊ฐ’์„ ์‚ฌ์šฉํ•˜์—ฌ ์ฃผ๋ฌธํ•œ ๋งค์žฅ์˜ ์ด๋ฆ„๊ณผ ์ฃผ์†Œ ๊ทธ๋ฆฌ๊ณ  ์ฃผ๋ฌธํ•œ ์Œ๋ฃŒ๊ฐ€ ๋ช‡ ์ž”์ธ์ง€๋ฅผ ๋ณด์—ฌ์ฃผ๋„๋ก ํ•˜๊ณ , ContentState์˜ ์ƒํƒœ์— status๋ผ๋Š” ํ”ฝ์—… ์ƒํƒœ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ด์ฃผ์—ˆ๋‹ค.

status์˜ ์ƒํƒœ๋กœ๋Š” received(์ฃผ๋ฌธ์™„๋ฃŒ), preparing(์Œ๋ฃŒ ์ œ์กฐ ์ค‘), ready_for_pickup(์ œ์กฐ์™„๋ฃŒ / ํ”ฝ์—… ๊ฐ€๋Šฅ) 3๊ฐ€์ง€์˜ ์ƒํƒœ๋ฅผ ์‚ฌ์šฉํ•  ์˜ˆ์ •์ด๋‹ค.

struct OrderWidgetAttributes: ActivityAttributes {
    public struct ContentState: Codable, Hashable {
        var status: String
    }
    var name: String
    var address: String
    var count : Int
}

Runner์— OrderWidgetManager.swift ํŒŒ์ผ์„ ์ƒ์„ฑํ•ด์ฃผ์–ด ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ ๊ธฐ๋Šฅ์„ ์ฒ˜๋ฆฌํ•ด ์ฃผ๋„๋ก ํ•˜์ž.

import Foundation
import ActivityKit

@available(iOS 16.1, *)
struct OrderActivityManager {
    static var currentActivity: Activity<OrderWidgetAttributes>?
    ...
}

start ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ค์–ด์„œ OrderWidget์˜ ์—‘ํ‹ฐ๋น„ํ‹ฐ๋ฅผ ์‹œ์ž‘ํ•ด๋ณด์ž. Attributes์˜ ์ •์  ๋ฐ์ดํ„ฐ๋ฅผ ์ƒ์„ฑํ•˜๊ธฐ ์œ„ํ•ด name, address, count ๊ฐ’์„ ๋ฐ›์•„์˜ค๋„๋กœ ํ•˜๊ณ , ํ•ด๋‹น ์˜ˆ์ œ๋Š” ํ‘ธ์‹œ๋ฅผ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ์—…๋ฐ์ดํŠธํ•  ๊ฒƒ์ด์–ด์„œ ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ ํ† ํฐ์„ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ํ•ด์ฃผ์—ˆ๋‹ค.

ContentState์— ์„ ์–ธํ•œ status์˜ ์ดˆ๊ธฐ ๊ฐ’์€ received๋กœ ๋„ฃ์–ด์ฃผ๋ฉด ๋œ๋‹ค.

static func start(name:String, address:String, count:Int, completion: @escaping (String?) -> Void) {
	let attributes = OrderWidgetAttributes(name: name, address: address, count: count)
    let state = OrderWidgetAttributes.ContentState(status: "received")
    ...
}

์ด์ œ OrderWidget์˜ ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ๋ฅผ request๋กœ ์š”์ฒญํ•˜๊ณ  ์ƒ์„ฑํ•œ ์ •์  ๋ฐ์ดํ„ฐ์™€ ์ƒํƒœ, pushType(.token)์„ ์ง€์ •ํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

์—‘ํ‹ฐ๋น„ํ‹ฐ๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ์ƒ์„ฑ ๋ฐ ํ˜ธ์ถœ์ด ๋˜๊ณ ๋‚˜๋ฉด, ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ ์ „์šฉ ํ‘ธ์‹œ ํ† ํฐ์„ ๋ฐœ๊ธ‰ํ•ด์ค€๋‹ค. ๋งŒ์•ฝ์— pushType์ด nil ์ด์—ˆ๋‹ค๋ฉด, ํ† ํฐ์ด ๋ฐœ๊ธ‰ ๋˜์ง€ ์•Š๊ธฐ ๋–„๋ฌธ์—, ๊ทธ๋ƒฅ request๋งŒ ํ˜ธ์ถœํ•˜๋ฉด ๋œ๋‹ค.

        do {
            let activity = try Activity<OrderWidgetAttributes>.request(
                attributes: attributes,
                contentState: state,
                pushType: .token
            )
            currentActivity = activity
            Task {
                for await data in activity.pushTokenUpdates {
                    let token = data.map { String(format: "%02x", $0) }.joined()
                    print("๐Ÿ“ฆ Push token: \(token)")
                    completion(token)
                    break
                }
            }
        } catch {
            print("โŒ Failed to start Live Activity: \(error)")
            completion(nil)
        }

AppDelegate์—์„œ Native์™€ Flutter๊ฐ„์˜ ์ด๋ฒคํŠธ๋ฅผ ์–ธ๊ฒฐํ•ด ์ฃผ๋„๋ก ํ•˜์ž.
Flutter์—์„œ ๋„ค์ดํ‹ฐ๋ธŒ์— ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ ์‹œ์ž‘์„ ์š”์ฒญํ•˜๋ฉด ๋„ค์ดํ‹ฐ๋ธŒ์—์„œ๋Š” ์š”์ฒญ์„ ์‹œ์ž‘ํ•˜๊ณ  ๋น„๋™๊ธฐ๋กœ ํ† ํฐ์ด ๋ฐœ๊ธ‰๋˜๋ฉด ๋‹ค์‹œ Flutter์— ์ฝœ๋ฐฑ์„ ๋„˜๊ฒจ์ฃผ๋Š” ๊ตฌ์กฐ์ด๋‹ค.

@objc class AppDelegate: FlutterAppDelegate {
    override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        GeneratedPluginRegistrant.register(with: self)
        
        if let controller = window?.rootViewController as? FlutterViewController {
            configureLiveActivityChannel(controller: controller)
        }
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }
    ...
}

์ฑ„๋„๋ช…์€ live_activity_order๋ผ๊ณ  ํ•ด์ฃผ์—ˆ๋‹ค. ์ฑ„๋„๋ช…์ด๋‚˜ ํด๋ž˜์Šค๋ช…์€ ์ž์œ ๋กญ๊ฒŒ ์‚ฌ์šฉํ•˜์…”๋„ ๋ฉ๋‹ˆ๋‹ค.

start ๋ฉ”์„œ๋“œ ์š”์ฒญ์‹œ ์ •์  ๋ฐ์ดํ„ฐ๋ฅผ ํŒŒ์‹ฑํ•˜๊ณ  ์•ž์„œ ๋งŒ๋“  OrderActivityManager ํด๋ž˜์Šค์— start ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•ด ์ค€ ๋’ค, ๊ฒฐ๊ณผ๋ฅผ ๋‹ค์‹œ Flutter๋กœ ๋ฐ˜ํ™˜ํ•ด ์ฃผ๋„๋ก ํ•˜์ž. ์—ฌ๊ธฐ์„œ ๊ฒฐ๊ณผ๋Š” ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ ์ „์šฉ ํ‘ธ์‹œ ํ† ํฐ์ด๋‹ค.

private func configureLiveActivityChannel(controller: FlutterViewController) {
        let order = FlutterMethodChannel(name: "live_activity_order", binaryMessenger: controller.binaryMessenger)
        
        order.setMethodCallHandler { call, result in
            switch call.method {
            case "start":
                guard let args =  call.arguments as? [String:Any],
                      let name = args["name"] as? String,
                      let address = args["address"] as? String,
                      let count = args["count"] as? Int else {
                    result(nil)
                    return
                }
                OrderActivityManager.start(name: name, address: address, count: count) { token in
                    result(token)
                }
            default:
                result(nil)
            }
        }
    }

Flutter๋กœ ์™€์„œ ๋™์ผ ์ฑ„๋„๋ช…์œผ๋กœ ๋„ค์ดํ‹ฐ๋ธŒ ์ฑ„๋„์„ ์—ฐ๊ฒฐํ•˜๊ณ  start ๋ฉ”์„œ๋“œ์™€ ํ•จ๊ป˜ ๋ฐ์ดํ„ฐ๋ฅผ ๋„˜๊ฒจ์ฃผ๋ฉด ๋œ๋‹ค.

ํ˜ธ์ถœํ•ด๋ณด๋ฉด, ์ •์ƒ์ ์œผ๋กœ ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ๊ฐ€ ์‹คํ–‰๋˜๊ณ  ํ† ํฐ์ด ๋ฐ˜ํ™˜๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

static const MethodChannel order = MethodChannel("live_activity_order");
 
final String? token = await order.invokeMethod("start", {
          "name": "The Original Starbucks",
          "address": "1912 Pike Place, Seattle",
          "count": 3,
});

pushType์„ nil๋กœ๋„ ๋ณ€๊ฒฝํ•ด์„œ ํ…Œ์ŠคํŠธ ํ•ด๋ณด๋ฉด, ํ† ํฐ์„ ๋ฐ˜ํ™˜ํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ์„ ๊ฒƒ์ด๋‹ค.

์ฐธ๊ณ ๋กœ UI์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ์—๋Š” Attributes๋Š” context.attributes ๋กœ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๊ณ , ContentState๋Š” context.state๋กœ ์ ‘๊ทผํ•ด์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

์˜ˆ์ œ์—์„œ ์‚ฌ์šฉํ•˜๋Š” UI๊ด€๋ จ ํŒŒ์ผ๊ณผ ์ด๋ฒคํŠธ ๊ด€๋ จ ํŒŒ์ผ์€ ๊ธ€ ๋งˆ์ง€๋ง‰์— ๊ณต์œ ํ•ด ๋†“๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. ํ•„์š”ํ•˜์‹œ๋‹ค๋ฉด ์ฐธ๊ณ ํ•˜์…”๋„ ๋ฉ๋‹ˆ๋‹ค.

Updating Live Activity States

๋‹ค์Œ์œผ๋กœ ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ ๊ธฐ๋Šฅ์˜ ํ•ต์‹ฌ์ด๋ผ๊ณ  ํ•  ์ˆ˜ ์žˆ๋Š” ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ์— ๋Œ€ํ•ด์„œ ์•Œ์•„๋ณด๋„๋ก ํ•˜์ž.

LiveActivity๋Š” ๋ง๊ทธ๋Œ€๋กœ ์‹ค์‹œ๊ฐ„ ํ˜„ํ™ฉ์„ ๋‚˜ํƒ€๋‚ด๋Š” ๊ธฐ๋Šฅ์ด๋‹ค. ๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์—, ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ€๊ฒฝํ•ด ์ฃผ์–ด์•ผ ์ œ๋Œ€๋กœ๋œ ๊ธฐ๋Šฅ์ด๋ผ๊ณ  ๋งํ•  ์ˆ˜ ์žˆ์„ ๊ฒƒ์ด๋‹ค.

๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ๋Š” ์•ฑ์˜ ๋ผ์ดํ”„์‚ฌ์ดํด๊ณผ ๋ณ„๊ฐœ๋กœ ์•ฑ์ด ์ข…๋ฃŒ๋˜์–ด๋„ ์ž ๊ธˆํ™”๋ฉด๊ณผ ๋‹ค์ด๋‚˜๋ฏน์•„์ผ๋žœ๋“œ์— ์ง€์†์ ์œผ๋กœ ๋…ธ์ถœ๋˜๋Š” ๊ตฌ์กฐ๋กœ ๋งŒ๋“ค์–ด์ ธ ์žˆ๋‹ค. ๊ฒฐ๊ตญ ์•ฑ์ด ์ข…๋ฃŒ ๋˜์–ด๋„ ์—…๋ฐ์ดํŠธ๊ฐ€ ๊ฐ€๋Šฅํ•˜์—ฌ์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์—, ๋กœ์ปฌ ์™ธ์—๋„ ํ‘ธ์‹œ๋ฅผ ์‚ฌ์šฉํ•œ ์—…๋ฐ์ดํŠธ๋ฅผ ์ง€์›ํ•˜๊ณ  ์žˆ๋‹ค.

๋กœ์ปฌ ? ํ‘ธ์‹œ ? ์–ด๋–จ ๋•Œ์— ์‚ฌ์šฉํ•ด์•ผ ํ• ๊นŒ ?

์•ž์„œ ๋งŒ๋“ค๊ณ  ์žˆ๋Š” ํ”ฝ์—… ์ฃผ๋ฌธ ํ˜„ํ™ฉ๊ณผ ๊ฐ™์€ ๊ธฐ๋Šฅ์€ ๋ฌด์กฐ๊ฑด ์„œ๋ฒ„ ํ‘ธ์‹œ๋ฅผ ํ†ตํ•ด์„œ๋งŒ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•ด์•ผ ํ•œ๋‹ค. ์•ฑ์€ ํ”ฝ์—… ์ฃผ๋ฌธ์ด ์–ด๋–ป๊ฒŒ ๋˜๋Š”์ง€๋ฅผ ์•Œ ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์— ์•ฑ์ด ์‹คํ–‰ ์ค‘์ด๋ผ๊ณ  ํ•ด๋„ ์ƒํƒœ ๋ณ€๊ฒฝ์˜ ๊ด€๋ฆฌ๋Š” ์„œ๋ฒ„์— ์žˆ๋‹ค๊ณ  ํ•  ์ˆ˜ ์žˆ๋‹ค. ํ•˜์ง€๋งŒ ๊ฑด๊ฐ•, ์œ„์น˜์™€ ๊ด€๋ จ๋œ ๋””๋ฐ”์ด์Šค์˜ ๊ธฐ๋Šฅ์„ ํ™œ์šฉํ•˜๋Š” ๊ฒฝ์šฐ ์ฃผ์ฒด๋Š” ๋กœ์ปฌ์— ์žˆ์„ ๊ฒƒ์ด๋‹ค. ์ด ๊ฒฝ์šฐ๋Š” ๋กœ์ปฌ์—์„œ ์‹ค์‹œ๊ฐ„ ํ˜„ํ™ฉ์˜ ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋ฉด ๋œ๋‹ค.

๋จผ์ €, ๋กœ์ปฌ์—์„œ ์—‘ํ‹ฐ๋น„ํ‹ฐ์˜ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด์„œ ์•Œ์•„๋ณด๋„๋ก ํ•˜์ž.

OrderActivityManager ํŒŒ์ผ์— update ํ•จ์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ•ด ์ฃผ์ž.

ActivityKit์˜ update๋Š” ๋งค์šฐ ๊ฐ„๋‹จํ•œ๋ฐ, ContentState์˜ ์„ ์–ธ๋œ ๋ฐ์ดํ„ฐ๋งŒ ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ๋กœ ๋ณ€๊ฒฝํ•ด์„œ ์š”์ฒญํ•˜๋ฉด ๋์ด๋‹ค.

Flutter์—์„œ ์—…๋ฐ์ดํŠธ ์š”์ฒญ์„ ๋ฐ›์•„์˜ค๊ธฐ ์œ„ํ•ด upate ํ•จ์ˆ˜์— ๋ณ€๊ฒฝํ•˜๊ณ ์ž ํ•˜๋Š” status ๊ฐ’์„ ๋ฐ›์•„์˜ฌ ์ˆ˜ ์žˆ๋„๋ก ํ•˜๊ณ , ํ•ด๋‹น ๊ฐ์ฒด์— ์ด๋ฏธ ์ƒ์„ฑํ•œ currentActivity์˜ update์— ์ƒˆ๋กœ์šด ContentState๋ฅผ ์ œ๊ณตํ•ด์ฃผ๋ฉด ์ฆ‰์‹œ ์ƒํƒœ๊ฐ€ ๋ณ€๊ฒฝ๋œ๋‹ค.

    static func update(status: String) {
        guard let activity = currentActivity else {
            print("โŒ Update failed: no active Live Activity found")
            return
        }

        let newState = OrderWidgetAttributes.ContentState(status: status)
        Task {
            await activity.update(using: newState)
            print("โœ… Live Activity Updated: \(status)")
        }
    }

AppDelegate ๋„ค์ดํ‹ฐ๋ธŒ ์ฑ„๋„ ์ผ€์ด์Šค์— update ๋ฉ”์„œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ด ์ฃผ๋„๋กํ•˜์ž.

        order.setMethodCallHandler { call, result in
            switch call.method {
            case "start":
                ...
            case "update":
                guard let args = call.arguments as? [String:Any],
                      let status = args["status"] as?  String else {
                    result(nil)
                    return
                }
                OrderActivityManager.update(status:status)
                result(nil)
            default:
                result(nil)
            }
        }

Flutter์—์„œ ์•„๋ž˜ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ€๊ฒฝํ•ด๋ณด์ž. ์ฐธ๊ณ ๋กœ ์ด๋ฏธ ์ƒ์„ฑ๋œ LiveActivity๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์— ๋ฐ˜๋“œ์‹œ start๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ํ™œ์„ฑํ™”ํ•œ ๋’ค ์—…๋ฐ์ดํŠธ๋ฅผ ์ง„ํ–‰ํ•˜์—ฌ์•ผ ํ•œ๋‹ค.

static Future<void> update(String status) async {
	await order.invokeMethod("update", {"status": status});
}

์ •์ƒ์ ์œผ๋กœ ๋กœ์ปฌ์—์„œ LiveActivity์˜ ์ƒํƒœ๊ฐ€ ์—…๋ฐ์ดํŠธ ๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

์ด์–ด์„œ ์„œ๋ฒ„์—์„œ ํ‘ธ์‹œ๋ฅผ ํ†ตํ•ด LiveActivity์˜ ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด์„œ ์•Œ์•„๋ณด๋„๋ก ํ•˜์ž.

APNs(Apple Push Notification service)๋Š” Apple์˜ ํ‘ธ์‹œ ์•Œ๋ฆผ์„ ์ „์†กํ•˜๋Š” ์„œ๋น„์Šค์ธ๋ฐ, ํ•ด๋‹น ์„œ๋น„์Šค์—์„œ ์ œ๊ณตํ•ด์ฃผ๋Š” ๊ธฐ๋Šฅ ์ค‘ ํ•˜๋‚˜๊ฐ€ push-to-update ์ฆ‰, ํ‘ธ์‹œ๋ฅผ ํ†ตํ•ด ํŠน์ • ๊ธฐ๋Šฅ๋“ค์˜ ์—…๋ฐ์ดํŠธ๋ฅผ ์ œ๊ณตํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ๋„ APNs์—์„œ ์ œ๊ณตํ•˜๋Š” ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ ์ „์šฉ ํ‘ธ์‹œ๋ฅผ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๋ฐ˜๋“œ์‹œ Live Activity ์ „์šฉ ํ† ํฐ(Token)์ด ์žˆ์–ด์•ผ์ง€๋งŒ ํ‘ธ์‹œ๋ฅผ ์ „์†กํ•  ์ˆ˜ ์žˆ๋‹ค.

๊ฒฐ๊ตญ ์•ฑ์—์„œ ํŠน์ • ์กฐ๊ฑด์— ์˜ํ•ด ์‹ค์‹œ๊ฐ„ ํ˜„ํ™ฉ์„ ์ œ๊ณตํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด, ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ ์‹œ์ž‘์„ ์š”์ฒญํ•˜๊ณ  ์ „์šฉ ํ† ํฐ(Token)์„ ๋ฐœ๊ธ‰ ๋ฐ›์•„ ์„œ๋ฒ„๋กœ ํ† ํฐ ์ •๋ณด๋ฅผ ๋„˜๊ฒจ์ฃผ์–ด, ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ์‹œ ์„œ๋ฒ„์—์„œ ์—…๋ฐ์ดํŠธํ•˜๊ณ ์ž ํ•˜๋Š” ๋‚ด์šฉ์„ ํ‘ธ์‹œ๋กœ ์ „์†กํ•˜์—ฌ ์‹œ์Šคํ…œ์ด ํ‘ธ์‹œ ์•Œ๋ฆผ์„ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ€๊ฒฝํ•˜๋Š” ํ๋ฆ„์ธ ๊ฒƒ์ด๋‹ค.

์‚ฌ์‹ค ๋ณต์žกํ•ด ๋ณด์ด์ง€๋งŒ ์ด๋ฏธ Apple์—์„œ๋Š” ์ฃผ๋กœ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ์‹๋“ค์ด๋‹ค.

ํ‘ธ์‹œ๋ฅผ ์–ด๋–ป๊ฒŒ ์ „์†กํ•ด์•ผ LiveActivity ์ „์šฉ ํ‘ธ์‹œ๋กœ ์ „์†ก ๋˜๋Š”์ง€, ์ž์„ธํžˆ ์‚ดํŽด๋ณด๋„๋ก ํ•˜์ž.

์•„๋ž˜ payload๊ฐ€ ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ ์ „์šฉ ํ‘ธ์‹œ ์ „์†ก์„ ์œ„ํ•ด ์„ธํŒ…ํ•˜๋Š” ๊ฐ’๋“ค์ด๋‹ค. ๊ธฐ๋ณธ์ ์œผ๋กœ ์ผ๋ฐ˜ ํ‘ธ์‹œ ์•Œ๋ฆผ์— ์žˆ๋Š” notification, data ๋“ฑ์˜ ํ•„๋“œ๋Š” ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ , apns ํ•„๋“œ๋ฅผ ์‚ฌ์šฉํ•ด ์ฃผ์–ด์•ผ ํ•œ๋‹ค.

FCM์ด ์•„๋‹Œ ๊ฒฝ์šฐ payload ๊ตฌ์กฐ๊ฐ€ ์ผ๋ถ€ ๋‹ค๋ฅผ ์ˆ˜ ์žˆ์œผ๋‹ˆ, ๋ณธ์ธ์ด ์‚ฌ์šฉํ•˜๋Š” ํ‘ธ์‹œ ์„œ๋น„์Šค์˜ ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ํ™•์ธ ํ•˜์…”์•ผ ํ•œ๋‹ค.

{
  "message": {
    "token": <device_push_token>,
    "apns": {
      "live_activity_token": <live_activity_token>,
      "headers": {
        "apns-priority": "10"
      },
      "payload": {
        "aps": {
          "timestamp": <timestamp>,
          "event": <event>,
          "content-state": {
          		...
				},
        "alert": {
            "title": <push_alarm_title>,
            "body": <push_alarm_body>
					}
				}
			}
		}  
	}
}

token์€ FCM์—์„œ ๋ฐœ๊ธ‰๋ฐ›๋Š” ๋””๋ฐ”์ด์Šค ํ† ํฐ ๊ฐ’์— ํ•ด๋‹น๋œ๋‹ค. apns.live_activity_token์—๋Š” ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ ์‹œ์ž‘์‹œ ๋ฐœ๊ธ‰ ๋ฐ›์•˜๋˜ ํ† ํฐ์„ ๋„ฃ์–ด์ฃผ๋ฉด ๋œ๋‹ค.

event ํ•„๋“œ๋Š” "update"์™€ "end"๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š”๋ฐ, ์ง€๊ธˆ์€ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•˜๋Š” ์ž‘์—…์„ ํ•˜๊ณ  ์žˆ์œผ๋‹ˆ update๋กœ ํ•ด์ฃผ๋„๋ก ํ•˜์ž.

alert ํ•„๋“œ์— title๊ณผ body๋ฅผ ํฌํ•จํ•˜๊ฒŒ ๋˜์–ด์žˆ๋Š”๋ฐ, alert๋Š” ํ•„์ˆ˜ ํ•„๋“œ์—ฌ์„œ ๋ฐ˜๋“œ์‹œ ๊ฐ’์„ ํฌํ•จํ•ด์•ผ ํ•˜๊ณ , ๋ฏธํฌํ•จ์‹œ ๊ฐ„ํ—์ ์œผ๋กœ ์—…๋ฐ์ดํŠธ๊ฐ€ ์ž‘๋™ํ•˜์ง€ ์•Š์„ ์ˆ˜๋„ ์žˆ๋‹ค.

์ด์ œ ๋งˆ์ง€๋ง‰์œผ๋กœ timestamp ํ•„๋“œ๋Š” iOS ์‹œ์Šคํ…œ์ด ํƒ€์ž„๋ผ์ธ์— ์˜ํ•ด ์„œ๋น„์Šค์˜ ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๊ธฐ ์œ„ํ•œ ํ•„๋“œ๋กœ, ์ด๋ฏธ ์‹œ๊ฐ„์ด ์ง€๋‚˜๊ฒŒ ๋˜๋ฉด ์—…๋ฐ์ดํŠธ๊ฐ€ ์ œ์™ธ๋˜๊ธฐ ๋•Œ๋ฌธ์—, ์ผ๋ฐ˜์ ์œผ๋กœ 1๋ถ„ ํ›„์˜ ํƒ€์ž„์Šคํƒฌํ”„๋ฅผ ์ƒ์„ฑํ•ด์„œ ์š”์ฒญํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

์ด์ œ ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ๋ฅผ ์‹œ์ž‘ํ•˜์—ฌ ํ† ํฐ์„ ์–ป๊ณ  ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ ์ „์šฉ ํ‘ธ์‹œ์˜ ์ƒํƒœ๋ฅผ preparing์œผ๋กœ ์ „์†กํ•ด๋ณด๋ฉด ์ƒํƒœ๊ฐ€ ๋ณ€๊ฒฝ๋œ ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ ์ „์šฉ ํ‘ธ์‹œ๋ฅผ ํ…Œ์ŠคํŠธํ•  ํ™˜๊ฒฝ์ด ์—†๋Š” ๊ฒฝ์šฐ์—๋Š” ๋Œ€์‹œ๋ณด๋“œ์—์„œ๋Š” ๋ถˆ๊ฐ€๋Šฅํ•˜๊ณ , ํฌ์ŠคํŠธ๋งจ์œผ๋กœ ๊ฐ„๋‹จํ•˜๊ฒŒ ํ…Œ์ŠคํŠธ๋ฅผ ํ•ด๋ณผ ์ˆ˜ ์žˆ๋‹ค.

Sending Live Activity Push via Postman

ํฌ์ŠคํŠธ๋งจ์— ์ ‘์†ํ•œ ํ›„ POST๋กœ ์š”์ฒญ ํ˜•์‹์„ ๋ณ€๊ฒฝํ•˜๊ณ  ์•„๋ž˜ ์ฃผ์†Œ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์ž.

Use this endpoint:
https://fcm.googleapis.com/v1/projects/<your_firebase_project_id>/messages:send

์ด์ œ ์‚ฌ์šฉ์ž ์ธ์ฆ์„ ์ง„ํ–‰ํ•ด์•ผ ํ•œ๋‹ค. ์ด๊ฑด ๋ณดํ†ต ๋งŒ๋ฃŒ ์‹œ๊ฐ„์ด ์žˆ์œผ๋‹ˆ 401์—๋Ÿฌ ๋ฐœ์ƒ์‹œ ์ธ์ฆ ์ ˆ์ฐจ๋ฅผ ์ˆ˜ํ–‰ํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

error": {
	"code": 401,
	"message": "Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.",
        "status": "UNAUTHENTICATED"
}

Authorization ํƒญ์œผ๋กœ ์ด๋™ํ•ด์ฃผ์ž.

Auth Type์˜ ์…€๋ ‰ํŠธ ๋ฐ•์Šค๋ฅผ ์—ด์–ด "Firebase Cloud Messaging API"๋ฅผ ์„ ํƒํ•ด์ฃผ์ž.

Authorize ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด, ์ž๋™์œผ๋กœ ๊ตฌ๊ธ€ ๋กœ๊ทธ์ธ์ด ์ง„ํ–‰๋˜๊ณ  ํ›„์— ์ธ์ฆ์— ์„ฑ๊ณตํ•˜๋ฉด ์ž๋™์œผ๋กœ Auth credentials์˜ ์ž„์‹œ ํ† ํฐ์ด ๋ฐœ๊ธ‰๋˜๊ฒŒ ๋œ๋‹ค.

๊ตฌ๊ธ€ ๊ณ„์ •์€ ํ•ด๋‹น ํ”„๋กœ์ ํŠธ ํŒ€์— ํฌํ•จ๋˜์–ด ์žˆ๋Š” ๊ณ„์ •์ด์–ด์•ผ ํ•œ๋‹ค !

์ธ์ฆ์ด ์™„๋ฃŒ ๋˜์—ˆ๋‹ค๋ฉด, Body ํƒญ์œผ๋กœ ์ด๋™ํ•˜์—ฌ raw+JSON์œผ๋กœ ์„ ํƒํ•˜์—ฌ ์œ„์—์„œ ์‚ดํŽด๋ณธ payload๋ฅผ ๋„ฃ์–ด์ฃผ๋„๋ก ํ•˜์ž.

timestamp ํ•„๋“œ์— ํƒ€์ž„์Šคํƒฌํ”„๋ฅผ ๊ณ„์† 1๋ถ„ ํ›„๋กœ ๊ณ„์‚ฐํ•ด์„œ ๋„ฃ์–ด์ฃผ๊ธฐ ๊ท€์ฐฎ๊ธฐ ๋•Œ๋ฌธ์—, ์ž๋™์œผ๋กœ 1๋ถ„ ํ›„์˜ ํƒ€์ž„์Šคํƒฌํ”„๋ฅผ ์ƒ์„ฑํ•˜๋Š” ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ถ”๊ฐ€ํ•ด ์ฃผ๋„๋ก ํ•˜์ž.

Scripts ํƒญ์˜ ์šฐ์ธก์— Pre-request๋ฅผ ์„ ํƒํ•˜์—ฌ ์•„๋ž˜ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

// ํ˜„์žฌ UTC ์‹œ๊ฐ„ + 60์ดˆ โ†’ Unix timestamp๋กœ ๋ณ€ํ™˜
const timestamp = Math.floor((Date.now() + 60000) / 1000);
pm.environment.set("LIVE_ACTIVITY_TIMESTAMP", timestamp);

์ด์ œ payload์˜ timestamp ํ•„๋“œ์˜ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ๋„ฃ์–ด์ฃผ๋ฉด ๋œ๋‹ค.

"timestamp": {{LIVE_ACTIVITY_TIMESTAMP}},

๋งˆ์ง€๋ง‰์œผ๋กœ ์„œ๋ฒ„ ํ‘ธ์‹œ ์—…๋ฐ์ดํŠธ์™€ ๊ด€๋ จํ•ด์„œ, ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ ์ „์šฉ ํ‘ธ์‹œ์˜ payload๋ฅผ ๊ตฌ์„ฑํ•  ๋•Œ์—๋Š” ๋ฐ˜๋“œ์‹œ ์ผ๋ฐ˜ ํ‘ธ์‹œ์— ์‚ฌ์šฉ๋˜๋Š” data, notification ๋“ฑ์˜ ํ•„๋“œ๋Š” ์ ˆ๋Œ€ ํฌํ•จํ•˜์‹œ๋ฉด ์•ˆ๋ฉ๋‹ˆ๋‹ค.
์ผ๋ฐ˜ ํ‘ธ์‹œ์˜ ํ•„๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ๋„ ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ ํ† ํฐ์ด ์žˆ๋‹ค๋ฉด, FCM์ด ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ ์ „์šฉ์ด๋ผ๊ณ  ๋‹จ์ •ํ•ด์„œ ํ‘ธ์‹œ๋ฅผ ๋“ฑ๋กํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ผ๋ฐ˜ ํ‘ธ์‹œ๋„ ๊ฑฐ์ ˆ๋˜๊ณ , ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ ํ‘ธ์‹œ๋„ ๊ฑฐ์ ˆ๋˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ์˜ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•จ๊ณผ ๋™์‹œ์— ํ‘ธ์‹œ ์•Œ๋ฆผ์„ ๋…ธ์ถœํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด, ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ ํ‘ธ์‹œ์™€ ์ผ๋ฐ˜ ํ‘ธ์‹œ๋ฅผ ๊ฐ๊ฐ ๊ฐœ๋ณ„์ ์œผ๋กœ ์š”์ฒญํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์—ฌ๊ธฐ๊นŒ์ง€ ์ž˜ ๋”ฐ๋ผ์™”๋‹ค๋ฉด, Postman์œผ๋กœ ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ ์ „์šฉ ํ‘ธ์‹œ๋ฅผ ๋ณด๋‚ด๊ณ , ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ๊ฐ€ ์—…๋ฐ์ดํŠธ ๋˜๋Š” ๊ฒƒ๊นŒ์ง€ ๋ฌธ์ œ ์—†์ด ์„ฑ๊ณตํ–ˆ์„ ๊ฒƒ์ด๋ผ ์ƒ๊ฐํ•œ๋‹ค. ํฌ์ŠคํŠธ๋งจ์€ ํ…Œ์ŠคํŠธ ์šฉ์ด๊ธฐ ๋•Œ๋ฌธ์—, ์‹ค์ œ ์„œ๋ฒ„์—์„œ ์ „์†ก๋˜๋Š” ๊ฒƒ๋„ ๋ฐ˜๋“œ์‹œ ํ…Œ์ŠคํŠธ ํ•ด๋ณด์…”์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๋งŒ์ผ ์ œ๋Œ€๋กœ ์ž‘๋™์ด ์•ˆ๋˜์‹œ๋Š” ๋ถ„๋“ค์€ ๋‹ค์‹œ ๋ฐ˜๋ณตํ•ด์„œ ํ•ด๋ณด์‹œ๊ณ  ๊ทธ๋ž˜๋„ ์•ˆ๋œ๋‹ค๋ฉด ๋ฌธ์˜ ๋‚จ๊ฒจ์ฃผ์„ธ์š”. ์ตœ๋Œ€ํ•œ ์ž์„ธํ•˜๊ฒŒ ๋‚จ๊ฒจ์ฃผ์…”์•ผ ํ•ด๊ฒฐ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค !!

Ending and Deactivating a Live Activity

์ด์–ด์„œ ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ์˜ ์ƒํƒœ๋ฅผ ์ข…๋ฃŒ ๋ฐ ์ œ๊ฑฐํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด์„œ ์•Œ์•„๋ณด๋„๋ก ํ•˜์ž.
์—ฌ๊ธฐ์„œ๋„ Update์™€ ๋™์ผํ•˜๊ฒŒ ๋กœ์ปฌ & ์„œ๋ฒ„ ๋‘˜ ๋‹ค ๊ฐ€๋Šฅํ•˜๋ฉฐ, ์—…๋ฐ์ดํŠธ์™€ ๋‹ค๋ฅธ ์ ์ด ์žˆ๋‹ค๋ฉด ์–ธ์ œ ์ œ๊ฑฐํ• ์ง€?๋ฅผ ์š”์ฒญํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๋ถ€๋ถ„์ด๋‹ค.

๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ์˜ ์ตœ๋Œ€ ๋…ธ์ถœ์‹œ๊ฐ„์€ 8์‹œ๊ฐ„์œผ๋กœ ์ข…๋ฃŒ ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•œ ํ›„ ๋ถ€ํ„ฐ 4์‹œ๊ฐ„ ๊นŒ์ง€ ์ž ๊ธˆํ™”๋ฉด์— ๋‚จ์•„์žˆ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ฆ‰ ์ตœ๋Œ€ 12์‹œ๊ฐ„๊นŒ์ง€ ๋…ธ์ถœ์ด ๊ฐ€๋Šฅํ•˜๊ธฐ ๋•Œ๋ฌธ์—, ์ฆ‰์‹œ ์ œ๊ฑฐํ•˜์—ฌ๋„ ๋˜๊ณ  ์ตœ๋Œ€ ์‹œ๊ฐ„๊นŒ์ง€ ๋˜๋Š” n๋ถ„ ํ›„, n์‹œ๊ฐ„ ํ›„ ๊นŒ์ง€ ๋‚จ๊ฒจ๋†“์„ ์ˆ˜ ์žˆ๋‹ค.

๊ทธ๋ ‡๋‹ค๋ฉด, ๋‹ค์ด๋‚˜๋ฏน์•„์ผ๋žœ๋“œ๋Š” ์–ด๋–ป๊ฒŒ ๋ ๊นŒ ? ๋‹ค์ด๋‚˜๋ฏน์•„์ผ๋žœ๋“œ๋Š” ์—…๋ฐ์ดํŠธ๊ฐ€ ์œ ์ง€๋˜๋Š” ๊ฒฝ์šฐ์—๋งŒ ํ™œ์„ฑํ™”๋˜๊ณ , ์ข…๋ฃŒ ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒ์‹œ ์ฆ‰์‹œ ์ œ๊ฑฐ๋ฉ๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ์ด์œ ๋กœ ์‹ค์‹œ๊ฐ„ ํ˜„ํ™ฉ์˜ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ชจ๋‘ ์—…๋ฐ์ดํŠธ๊ฐ€ ๋œ ๊ฒฝ์šฐ๋ผ๋ฉด ๋ฐ˜๋“œ์‹œ ์ข…๋ฃŒ ์ด๋ฒคํŠธ๋ฅผ ๋ฐœ์ƒ์‹œ์ผœ ๋ถˆํ•„์š”ํ•˜๊ฒŒ ๋‹ค์ด๋‚˜๋ฏน์•„์ผ๋žœ๋“œ์— ๋…ธ์ถœ๋˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•ด์•ผ ํ•œ๋‹ค.
์‹ค์‹œ๊ฐ„ ํ˜„ํ™ฉ์˜ ๋ณ€๋™์ด ๋” ์ด์ƒ ์—†๋Š”๋ฐ๋„ ๋ถˆ๊ตฌํ•˜๊ณ , ์ข…๋ฃŒ๋ฅผ ์‹œ์ผœ์ฃผ์ง€ ์•Š๋Š”๋‹ค๋ฉด, ์‚ฌ์šฉ์ž๊ฐ€ ์‹ค์‹œ๊ฐ„ ํ˜„ํ™ฉ์„ ๋น„ํ™œ์„ฑํ™” ํ•ด๋ฒ„๋ฆด ์ˆ˜๋„ ์žˆ์œผ๋‹ˆ ๋‹ค์ด๋‚˜๋ฏน์•„์ผ๋žœ๋“œ ์ œ๊ฑฐ๋ฅผ ์œ„ํ•ด์„œ๋ผ๋„ ์ข…๋ฃŒ ์ด๋ฒคํŠธ๋Š” ํ•„์š”ํ•˜๋‹ค.

์ถ”๊ฐ€๋กœ end ์ด๋ฒคํŠธ๋ฅผ ์š”์ฒญํ•˜๊ฒŒ ๋˜๋ฉด ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ์˜ ์ ‘๊ทผ ๊ถŒํ•œ์ด ์ž๋™ ์‚ญ์ œ๋˜๊ฒŒ ๋˜๋ฏ€๋กœ, update๋ฅผ ๋‹ค์‹œ ์š”์ฒญํ•˜๋”๋ผ๋„ ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ๋ฅผ ์ œ์–ดํ•  ์ˆ˜ ์—†๋‹ค. ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ ํ‘ธ์‹œ๋ฅผ ์‚ฌ์šฉํ•ด๋„ ๋™์ผํ•˜๊ฒŒ update๋Š” ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ค.

๋จผ์ € ๋กœ์ปฌ์—์„œ ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ์˜ ์ข…๋ฃŒ ๋ฐ ์ œ๊ฑฐํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์‚ดํŽด๋ณด๋„๋ก ํ•˜์ž.

์—…๋ฐ์ดํŠธ์™€ ๋™์ผํ•˜๊ฒŒ ActivityKit์˜ end๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค. end ์‚ฌ์šฉ์‹œ update์™€ ๋™์ผํ•˜๊ฒŒ ContentState์˜ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•ด์ค„ ์ˆ˜ ์žˆ๊ณ , ์ถ”๊ฐ€๋กœ dismissalPolicy๋ฅผ ์‚ฌ์šฉํ•ด ์ œ๊ฑฐ ์‹œ์ ์„ ์ œ์–ดํ•  ์ˆ˜๋„ ์žˆ๋‹ค.

dismissalPolicy์—๋Š” immediate, after ํƒ€์ž…์„ ์‚ฌ์šฉํ•˜์—ฌ ์ฆ‰์‹œ ์ œ๊ฑฐ์‹œํ‚ค๊ฑฐ๋‚˜ ์‹œ๊ฐ„ ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ค์–ด ์›ํ•˜๋Š” ์‹œ๊ฐ„ ํ›„์— ์ž๋™์œผ๋กœ ์ œ๊ฑฐ๋˜๊ฒŒ ์š”์ฒญํ•  ์ˆ˜ ์žˆ๋‹ค.

.immediate๋ฅผ ์‚ฌ์šฉํ•ด ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ๊ฐ€ ์ฆ‰์‹œ ์ œ๊ฑฐ๋  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ์ž. ์ด ๊ฒฝ์šฐ์—๋Š” ์ž ๊ธˆํ™”๋ฉด, ๋‹ค์ด๋‚˜๋ฏน์•„์ผ๋žœ๋“œ์—์„œ ์ฆ‰์‹œ ์ œ๊ฑฐ๋  ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์—, ContentState์˜ ์ƒํƒœ๋Š” ๋ณ€๊ฒฝํ•˜์ง€ ์•Š์•„๋„ ๋œ๋‹ค.

์ด์ œ ๋” ์ด์ƒ ํ•ด๋‹น ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ๋Š” ํ™œ์„ฑ ์ƒํƒœ๊ฐ€ ์•„๋‹ˆ๊ธฐ ๋•Œ๋ฌธ์— currentActivity๋„ ๋‹ค์‹œ NULL๋กœ ๋ณ€๊ฒฝํ•ด ์ฃผ์ž.

static func endImmediately() {
        guard let activity = currentActivity else {
            print("โŒ End failed: no active Live Activity found")
            return
        }

        Task {
            await activity.end(dismissalPolicy: .immediate)
            print("๐Ÿ›‘ Live Activity ended immediately")
            currentActivity = nil
        }
    }

.after๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์š”์ฒญ ์‹œ์ ์œผ๋กœ ๋ถ€ํ„ฐ์˜ ์ด ํ›„ ์‹œ๊ฐ„๋Œ€๋ฅผ ์ƒ์„ฑํ•œ ๋’ค์— ์š”์ฒญํ•ด์ฃผ๋ฉด ๋œ๋‹ค. ์ด ๊ฒฝ์šฐ์—๋Š” ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ๊ฐ€ ๋‹ค์ด๋‚˜๋ฏน์•„์ผ๋žœ๋“œ๋Š” ์ฆ‰์‹œ ์ œ๊ฑฐ ๋˜์ง€๋งŒ, ์ž ๊ธˆํ™”๋ฉด์—์„œ ์ฆ‰์‹œ ์ œ๊ฑฐ๋˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ContentState์˜ ์ƒํƒœ๋„ ํ•จ๊ป˜ ๋ณ€๊ฒฝํ•ด ์ฃผ์–ด์•ผ ํ•œ๋‹ค.

static func endAfterDelay(seconds: Int = 60) {
        guard let activity = currentActivity else {
            print("โŒ End failed: no active Live Activity found")
            return
        }

        let finalState = OrderWidgetAttributes.ContentState(status: "ready_for_pickup")
        let oneHourLater = Date().addingTimeInterval(TimeInterval(seconds))

        Task {
            await activity.end(using: finalState, dismissalPolicy: .after(oneHourLater))
            print("๐Ÿ•’ Live Activity scheduled to end after \(seconds) seconds (status: ready_for_pickup)")
            currentActivity = nil
        }
    }

AppDelegate์— MethodChannel์— case๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ์ด๋ฒคํŠธ๋ฅผ ์—ฐ๊ฒฐํ•ด ์ฃผ๋„๋ก ํ•˜์ž.

case "end_now":
	OrderActivityManager.endImmediately()
	result(nil)
case "end_later":
	OrderActivityManager.endAfterDelay()
	result(nil)

์ด์ œ ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ๋ฅผ ๋‹ค์‹œ ์‹œ์ž‘ํ•˜๊ณ  ์ด๋ฒคํŠธ๋ฅผ ์š”์ฒญํ•ด ๋ณด๋ฉด, ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ๊ฐ€ ์ข…๋ฃŒ ๋˜๋ฉด์„œ ๋” ์ด์ƒ ๋‹ค์ด๋‚˜๋ฏน์•„์ผ๋žœ๋“œ๋‚˜ ์ž ๊ธˆํ™”๋ฉด์— ํ‘œ์‹œ๋˜์ง€ ์•Š๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

1์‹œ๊ฐ„ ํ›„์— ์ข…๋ฃŒ๋˜๋Š” ์ด๋ฒคํŠธ๋Š” ๋‹ค์ด๋‚˜๋ฏน์•„์ผ๋žœ๋“œ์—์„œ๋Š” ์ฆ‰์‹œ ์ œ๊ฑฐ๋˜๊ณ , ์ž ๊ธˆํ™”๋ฉด์—์„œ๋Š” ์ •ํ™•ํžˆ 1์‹œ๊ฐ„ ํ›„์— ์ž๋™์œผ๋กœ ์ œ๊ฑฐ๋˜๋Š” ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ๋‹ค.

๋งŒ์•ฝ์— ์‹œ๊ฐ„ ํ•จ์ˆ˜๋ฅผ 10์‹œ๊ฐ„ ํ›„๋กœ ์ƒ์„ฑํ•˜๊ฒŒ ๋˜๋”๋ผ๋„, ์ข…๋ฃŒ ์ด๋ฒคํŠธ ๋ฐœ์ƒ ํ›„ ์ตœ๋Œ€ ๋…ธ์ถœ์‹œ๊ฐ„์€ 4์‹œ๊ฐ„์ด๋ผ ์‹œ์Šคํ…œ์ด 4์‹œ๊ฐ„ ํ›„์— ์ž๋™์œผ๋กœ ์ œ๊ฑฐํ•˜๊ฒŒ ๋œ๋‹ค.

.immediate .after

์ด์–ด์„œ ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ ์ „์šฉ ํ‘ธ์‹œ๋ฅผ ์‚ฌ์šฉํ•ด์„œ end ์ด๋ฒคํŠธ๋ฅผ ์ „์†กํ•ด๋ณด๋„๋ก ํ•˜์ž.

๊ธฐ๋ณธ์ ์ธ payload๋Š” update์™€ ๋™์ผํ•˜๊ณ , event๋ฅผ "end"๋กœ ๋ณ€๊ฒฝํ•˜๊ณ  ์ œ๊ฑฐ ์‹œ์ ์„ ์š”์ฒญํ•˜๋Š” dismissal-date ํ•„๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

"payload" : {
	"event" : "end",
    "content-state" : { ... },
	"dismissal-date" : <timestamp>
}

๋งŒ์ผ ์ตœ๋Œ€ ๋…ธ์ถœ์‹œ๊ฐ„๋™์•ˆ ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ๋ฅผ ์ž ๊ธˆํ™”๋ฉด์— ๋‚จ๊ฒจ๋†“๊ณ  ์‹ถ๋‹ค๋ฉด, dismissal-date๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์œผ๋ฉด ๋œ๋‹ค.

Starting a Live Activity from the Server

iOS 17.2 ๋ฒ„์ „์— ์ถ”๊ฐ€๋œ Push to Start ๊ธฐ๋Šฅ์— ๋Œ€ํ•ด์„œ ๊ฐ„๋‹จํ•˜๊ฒŒ ์ด๊ฒŒ ๋ญํ•˜๋Š” ๊ฑด์ง€๋งŒ ์•Œ์•„๋ณด๋„๋ก ํ•˜์ž. ์ €๋„ ์‹ค์ œ๋กœ ์šด์˜ ๋‹จ๊ณ„์—์„œ ์‚ฌ์šฉํ•œ ์ ์€ ์—†์–ด์„œ ์ž์„ธํ•œ ๋‚ด์šฉ์€ ์ถ”ํ›„์— ๋‹ค๋ฃจ๋„๋ก ํ•˜๊ฒ ๋‹ค.

Push to Start, ์„œ๋ฒ„ ํ‘ธ์‹œ๋ฅผ ํ†ตํ•ด ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ๋ฅผ ์‹œ์ž‘ํ•˜๋Š” ๊ธฐ๋Šฅ์ด๋‹ค.

ํ•ด๋‹น ๊ธฐ๋Šฅ์„ ์œ„ํ•ด์„œ๋Š” push-to-start token์„ ์ƒ์„ฑํ•ด์•ผ ํ•œ๋‹ค.

func observePushToStartToken() {
        if #available(iOS 17.2, *) {
            Task {
                for await data in Activity<YourAttributes>.pushToStartTokenUpdates {
                    let token = data.map { String(format: "%02x", $0) }.joined()
                    print("push-to-start token : \(token)")
                }
            }
        }
    }

ํ•ด๋‹น ํ† ํฐ์€ Attributes ๋งˆ๋‹ค ๊ณ ์œ ํ•œ ์Šคํƒ€ํŠธ ํ† ํฐ์ด๋ฏ€๋กœ, ์—ฌ๋Ÿฌ ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋‹ค๋ฉด ๊ฐœ๋ณ„์ ์œผ๋กœ ๊ด€๋ฆฌ๋ฅผ ํ•ด์•ผํ•œ๋‹ค.

payload๋Š” update์™€ ๋™์ผํ•˜๋ฉฐ, event๋ฅผ "start"๋กœ ์š”์ฒญํ•˜๊ณ , attributes-type๊ณผ attributes๋ฅผ ์ถ”๊ฐ€ ํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

"payload" : {
	"aps" : {
    	"event": "start",
        "attributes-type": <your_attributes>
        "attributes" : { ... }
    }
}

์ง€๊ธˆ๊นŒ์ง€ ์˜ˆ์ œ๋กœ ์‚ฌ์šฉํ•œ OrderWidgetAttributes๋ฅผ ์˜ˆ๋กœ ๋“ค์–ด๋ณด์ž๋ฉด, payload๋Š” ์•„๋ž˜์™€ ๊ฐ™์€ ๊ตฌ์„ฑ์ด ๋œ๋‹ค.

"payload" : {
	"aps" : {
    	"event": "start",
        "attributes-type": "OrderWidgetAttributes",
        "attributes": {
            "name": "Pickup store name",
            "address": "Pickup address",
            "count": 2
          },
    }
}

push-to-start ํ† ํฐ์€ ๊ธฐ์กด ์‚ฌ์šฉํ•˜๋˜ "live_activity_channel"์— ํ† ํฐ์œผ๋กœ ์š”์ฒญํ•˜์—ฌ ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ๋ฅผ ์‹œ์ž‘ํ•  ์ˆ˜ ์žˆ๋‹ค.

์„œ๋ฒ„์—์„œ ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ๋ฅผ ์‹œ์ž‘ํ•  ๋•Œ์— ์‚ฌ์šฉํ•˜๋Š” ์Šคํƒ€ํŠธ ํ† ํฐ์€ ์‹œ์ž‘์—๋งŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ณ , update์‹œ์—๋Š” ๊ธฐ์กด์— ๋ฐœ๊ธ‰๋ฐ›๋˜ ํ˜•ํƒœ๋กœ ๋‹ค์‹œ ์—‘ํ‹ฐ๋น„ํ‹ฐ๋งˆ๋‹ค ๊ณ ์œ ํ•œ ํ† ํฐ์„ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค.

Token(push-to-start) : ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ ์‹œ์ž‘ ์š”์ฒญ์‹œ, Attributes๋งˆ๋‹ค ๊ณ ์œ ํ•จ
Token(live-activity) : ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ ์—…๋ฐ์ดํŠธ ๋ฐ ์ข…๋ฃŒ ์š”์ฒญ์‹œ, ์—‘ํ‹ฐ๋น„ํ‹ฐ ๊ฐœ๋ณ„

ํ† ํฐ(Token)๊ณผ ๊ด€๋ จํ•ด์„œ ๋‹ค์‹œ ์ •๋ฆฌํ•ด ๋ณด์ž๋ฉด, ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ ์‚ฌ์šฉ์‹œ ํ•„์š”ํ•œ ํ† ํฐ์€ 3๊ฐœ๋กœ ๋‚˜๋‰œ๋‹ค.

๊ฐ๊ฐ Device Token, LiveActivity Token, LiveActivity PushToStart Token ์ด๋ ‡๊ฒŒ 3๊ฐ€์ง€๋ผ๊ณ  ๋ช…์นญ ํ•˜๊ฒ ๋‹ค.

Device Token์€ FCM์„ ๋ณด๋‚ด๊ธฐ ์œ„ํ•œ ๊ธฐ๊ธฐ๋งˆ๋‹ค ๊ณ ์œ ํ•œ ํ† ํฐ์ด๋‹ค. FCM ์ „์†ก์„ ์œ„ํ•œ ๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ ํ† ํฐ์ด๋ฉฐ, ๋ชจ๋“  FCM ๊ธฐ๋Šฅ์€ ํ•ด๋‹น ํ† ํฐ ์—†์ด๋Š” ์ „์†ก์ด ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ค.

์—ฌ๊ธฐ์„œ ํ•œ ๊ฐ€์ง€ ์˜๋ฌธ์ ์ด FCM ํ† ํฐ์ด ์—†๋‹ค๋ฉด, ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ ํ‘ธ์‹œ๋Š” ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋‚˜ ? Device Token์€ payload์˜ ํ•„์ˆ˜ ํ•„๋“œ์ด๊ธฐ ๋•Œ๋ฌธ์— ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋‹ค.
ํ˜ผ๋™ํ•˜์‹œ๋ฉด ์•ˆ๋˜๋Š” ๋ถ€๋ถ„์ด Device Token๊ณผ ์•ฑ ๋˜๋Š” ์‹œ์Šคํ…œ์˜ ์•Œ๋ฆผ ์„ค์ • ์—ฌ๋ถ€์™€ ๋ณ„๊ฐœ๋ผ๋Š” ๋ถ€๋ถ„์ด๋‹ค. ์ฆ‰ ์•ฑ์— ์•Œ๋ฆผ์„ ๊ฑฐ๋ถ€ํ•˜๋”๋ผ๋„ FCM ํ† ํฐ์€ ์ด๋ฏธ ์ƒ์„ฑ๋œ ์ƒํƒœ์ด๊ธฐ ๋–„๋ฌธ์—, ์—ฌ๋ถ€์™€ ๋ณ„๊ฐœ๋กœ ๋ฐœ๊ธ‰์€ ๊ฐ€๋Šฅํ•˜๋‹ค.

ํ˜„์žฌ ์„œ๋น„์Šค ๊ตฌ์กฐ๊ฐ€ ์•Œ๋ฆผ ์„ค์ • ์—ฌ๋ถ€๋ฅผ ํŒ๋‹จํ•ด์„œ ํ† ํฐ์„ ๋ฐœ๊ธ‰๋ฐ›์•„ ์„œ๋ฒ„์— ์ €์žฅ์‹œํ‚ค๋Š” ๊ตฌ์กฐ ์˜€๋‹ค๋ฉด, ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ ์‚ฌ์šฉ์„ ์œ„ํ•ด์„œ๋Š” ๋ฌด์กฐ๊ฑด ์•ฑ์—์„œ FCM ํ† ํฐ์„ ๋ฐœ๊ธ‰ ๋ฐ›์•„์•ผ ํ•œ๋‹ค.

๋‹ค์Œ์œผ๋กœ LiveActivity Token์ด๋‹ค. LiveActivity Token์€ ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ๋งˆ๋‹ค ๊ณ ์œ ํ•˜๊ฒŒ ์ƒ์„ฑ๋˜๋Š” ๊ฐœ๋ณ„ ํ† ํฐ์ด๋‹ค. ๋งŒ์•ฝ์— A๋ผ๋Š” ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ๋ฅผ 3๊ฐœ๋ฅผ ์š”์ฒญํ–ˆ๋‹ค๋ฉด, LiveActivity Token๋„ 3๊ฐœ๊ฐ€ ๋ฐœ๊ธ‰๋œ๋‹ค. ๊ทธ๋ ‡๋‹ค๋ฉด A๋ผ๋Š” ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ 2๊ฐœ, B๋ผ๋Š” ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ 3๊ฐœ๋ฅผ ์‹œ์ž‘ํ–ˆ๋‹ค๋ฉด, LiveActivity Token์€ ์ด 5๊ฐœ๊ฐ€ ๋ฐœ๊ธ‰๋˜๊ฒŒ ๋œ๋‹ค.

๋งˆ์ง€๋ง‰์œผ๋กœ LiveActivity PushToStart Token์€ ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ ์ข…๋ฅ˜์— ๋”ฐ๋ผ ๋ฐœ๊ธ‰๋˜๋Š” ํ† ํฐ์ด๋‹ค. A๋ผ๋Š” ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ์— ๋Œ€ํ•ด์„œ๋Š” ๋‹จ 1๊ฐœ์˜ ํ† ํฐ์ด ๋ฐœ๊ธ‰๋˜๋Š” ๊ตฌ์กฐ์ด๋‹ค. B๋ผ๋Š” ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ๊ฐ€ ์žˆ๋‹ค๋ฉด ์—ญ์‹œ B์—๋Œ€ํ•œ ํ† ํฐ 1๊ฐœ๊ฐ€ ์ƒ์„ฑ๋œ๋‹ค.
์•ž์„œ ์‚ดํŽด๋ณธ LiveActivity Token์€ ์ข…๋ฅ˜ ์ƒ๊ด€์—†์ด ๋ชจ๋“  ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ ์š”์ฒญ์‹œ ๋ฐœ๊ธ‰๋˜๋Š” ๋ฐ˜๋ฉด, PushToStart Token์€ ์ข…๋ฅ˜๋งˆ๋‹ค ๊ณ ์œ ํ•œ ํ† ํฐ์ธ ๊ฒƒ์ด๋‹ค.


Live Activities in Action: Beyond the Basics

Pickup ์ฃผ๋ฌธ ์‹ค์‹œ๊ฐ„ ํ˜„ํ™ฉ์„ ์˜ˆ์ œ๋กœ ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ์— ๋Œ€ํ•œ ์ „๋ฐ˜์ ์ธ ์‚ฌ์šฉ ๋ฐฉ๋ฒ•๋“ค๊ณผ ๊ตฌ์กฐ์— ๋Œ€ํ•ด์„œ ์‚ดํŽด๋ณด์•˜๋‹ค.

์ด๋ฒˆ์—๋Š” ์•ผ๊ตฌ ๊ฒฝ๊ธฐ ์‹ค์‹œ๊ฐ„ ํ˜„ํ™ฉ์„ ์˜ˆ์ œ๋กœ ์ข€ ๋” ๋‹ค์–‘ํ•˜๊ฒŒ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์—๋Š” ์–ด๋–ค ๊ฒƒ๋“ค์ด ์žˆ๋Š”์ง€ ๊ฐ€๋ณ๊ฒŒ ๋งŒ๋“ค์–ด๋ณด๋„๋ก ํ•˜์ž.

ScoreWidget์ด๋ผ๋Š” ์ƒˆ๋กœ์šด Widget Extension์„ ์ถ”๊ฐ€ํ•ด ์ฃผ์ž.

UI ๊ตฌ์„ฑ์„ ์œ„ํ•œ ์ •์  ๋ฐ์ดํ„ฐ์—๋Š” ๊ฐ ํŒ€์˜ ์ •๋ณด๊ฐ€ ํ•„์š”ํ•  ๊ฒƒ์ด๊ณ , ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ์„ ์œ„ํ•œ ContentState์—๋Š” ํ˜„์žฌ ๊ฒฝ๊ธฐ์˜ ์ƒํƒœ๊ฐ€ ํ•„์š”ํ•˜๊ฒŒ ๋œ๋‹ค.

๊ฐ๊ฐ ๋ชจ๋ธ๋กœ ๊ตฌ์„ฑํ•ด๋ณด์ž.

public struct ScoreStateModel:Codable, Hashable {
    let homeScore: Int
    let awayScore: Int
    let inning: String
    let status: String
}

public struct ScoreTeamModel: Codable, Hashable {
    let code:String
    let name:String
    let logo:String
}

๊ตฌ์„ฑํ•œ ๋ชจ๋ธ์„ ์‚ฌ์šฉํ•ด ScoreWidget์˜ Attributes๋ฅผ ๋ณ€๊ฒฝํ•ด ์ฃผ๋„๋ก ํ•˜์ž.

home, away๋Š” ํŒ€์˜ ์ •๋ณด์ด๊ธฐ ๋•Œ๋ฌธ์— ๊ฒฝ๊ธฐ๊ฐ€ ์‹œ์ž‘๋œ ํ›„์—๋Š” ๋ณ€๊ฒฝ๋˜์ง€ ์•Š์„ ๊ฒƒ์ด๊ณ  ScoreStateModel์—์„œ๋Š” home, away ํŒ€์˜ ์Šค์ฝ”์–ด์™€ ํ˜„์žฌ ์ด๋‹, ๊ฒฝ๊ธฐ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•ด ์ฃผ์–ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— status๋กœ ๋ณ€๊ฒฝํ•˜์˜€๋‹ค.

struct ScoreWidgetAttributes: ActivityAttributes {
    public struct ContentState: Codable, Hashable {
        var status: ScoreStateModel
    }
    var home: ScoreTeamModel
    var away: ScoreTeamModel
}

๊ฒฝ๊ธฐ ์ƒํƒœ๋Š” scheduled(๊ฒฝ๊ธฐ ์ „), inprogress(๊ฒฝ๊ธฐ ์ค‘), closed(๊ฒฝ๊ธฐ ์ข…๋ฃŒ), cancelled(์ทจ์†Œ) ์ด๋ ‡๊ฒŒ 4๊ฐ€์ง€ ์ƒํƒœ๋ฅผ ์‚ฌ์šฉํ•˜์˜€๋‹ค.

์•ž์„œ ๋ฐฐ์›Œ๋ณธ๋ฐ๋กœ Manager ํŒŒ์ผ์„ ๋งŒ๋“ค๊ณ  ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ๋ฅผ ์‹œ์ž‘ํ•  ์ˆ˜ ์žˆ๋„๋ก start ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ค์–ด์ฃผ์ž.

import ActivityKit

@available(iOS 16.1, *)
struct ScoreActivityManager {
    static var currentActivity: Activity<ScoreWidgetAttributes>?
    
    static func start(home:ScoreTeamModel, away:ScoreTeamModel,status:ScoreStateModel, completion: @escaping (String?) -> Void) {
        let attributes = ScoreWidgetAttributes(home: home, away: away)
        let state = ScoreWidgetAttributes.ContentState(status:status)
        
        do {
            let activity = try Activity<ScoreWidgetAttributes>.request(
                attributes: attributes,
                contentState: state,
                pushType: .token
            )
            currentActivity = activity
            Task {
                for await data in activity.pushTokenUpdates {
                    let token = data.map { String(format: "%02x", $0) }.joined()
                    print("๐Ÿ“ฆ Push token: \(token)")
                    completion(token)
                    break
                }
            }
        } catch {
            print("โŒ Failed to start Live Activity: \(error)")
            completion(nil)
        }
    }
}

ScoreWidgetAttributes์—๋Š” ๋ชจ๋ธ์„ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์—, Flutter์—์„œ ์ „์†กํ•œ ๊ฐ์ฒด๋ฅผ Swift์—์„œ ํŒŒ์‹ฑํ•ด์ฃผ์–ด์•ผ ํ•œ๋‹ค.

score.setMethodCallHandler { call, result in
            switch call.method {
            case "start":
                guard let args = call.arguments as? [String: Any],
                      let homeDict = args["home"] as? [String: Any],
                      let awayDict = args["away"] as? [String: Any],
                      let statusDict = args["status"] as? [String: Any] else {
                    result(nil)
                    return
                }
                
                do {
                    let homeData = try JSONSerialization.data(withJSONObject: homeDict)
                    let awayData = try JSONSerialization.data(withJSONObject: awayDict)
                    let statusData = try JSONSerialization.data(withJSONObject: statusDict)
                    
                    let home = try JSONDecoder().decode(ScoreTeamModel.self, from: homeData)
                    let away = try JSONDecoder().decode(ScoreTeamModel.self, from: awayData)
                    let status = try JSONDecoder().decode(ScoreStateModel.self, from: statusData)
                    
                    ScoreActivityManager.start(home: home, away: away, status: status) { token in
                        result(token)
                    }
                } catch {
                    result(nil)
                }
            default:
                result(nil)
            }
        }

๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ ์ „์šฉ ํ‘ธ์‹œ์˜ payload์˜ content-state ๊ตฌ์กฐ๋Š” ๊ฐ์ฒด ๊ตฌ์กฐ ๊ทธ๋Œ€๋กœ๋ฅผ ์‚ฌ์šฉํ•ด ์ „์†กํ•˜๋ฉด ๋œ๋‹ค.

๊ทธ ์™ธ์˜ ๋ชจ๋“  ๊ธฐ๋Šฅ์€ ๋™์ผํ•˜๋‹ค.

"content-state": {
	"status" : {
		"status":"inprogress",
		"homeScore" : 2,
		"awayScore" : 3,
		"inning" : "Top 4th"
	}
},

์ž ์ด์ œ ์—ฌ๋Ÿฌ๋ถ„๋“ค๋„ ์‹ค์‹œ๊ฐ„ ๊ฒฝ๊ธฐ ํ˜„ํ™ฉ์„ ๋ณด์—ฌ์ฃผ๋Š” ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๊ฒŒ ๋œ๊ฒƒ์ด๋‹ค !

๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ๋ฅผ ์ ์šฉํ•˜๊ณ  ์‹ถ์€๋ฐ, ํ‘ธ์‹œ๋ฅผ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๋Š” ํ™˜๊ฒฝ์ด ์•„๋‹ˆ๋ผ๋ฉด, ์‹œ์Šคํ…œ API ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜์—ฌ ์„œ๋ฒ„ ํ‘ธ์‹œ ์—†์ด๋„ ๋กœ์ปฌ์—์„œ ๋””๋ฐ”์ด์Šค ์œ„์น˜ ๋ณ€ํ™”์‹œ ์—…๋ฐ์ดํŠธ๋ฅผ ์š”์ฒญํ•˜์—ฌ ๋Ÿฌ๋‹, ๋งŒ๋ณด๊ธฐ ๋“ฑ์˜ ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ๋„ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค.

iOS์˜ CoreMotion + CoreLocation์„ ์‚ฌ์šฉํ•˜์—ฌ ์‚ฌ์šฉ์ž์˜ ์œ„์น˜ ๋ณ€ํ™”์‹œ ๋กœ์ปฌ์—์„œ ์—…๋ฐ์ดํŠธํ•˜์—ฌ ์‹ค์‹œ๊ฐ„ ํ˜„ํ™ฉ์„ ๋ณด์—ฌ์ฃผ๋Š” ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ์ด๋‹ค.

์ด์ฒ˜๋Ÿผ ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ฃผ์–ด์ง„ ๊ตฌ์กฐ์•ˆ์—์„œ ์–ผ๋งˆ๋“ ์ง€ ์ž์œ ๋กญ๊ฒŒ ์ปค์Šคํ…€ํ•  ์ˆ˜ ์žˆ๊ณ , ์›ํ•˜๋Š” ์‹ค์‹œ๊ฐ„ ์ •๋ณด๋ฅผ ๋น ๋ฅด๊ณ  ๊ฐ„ํŽธํ•˜๊ฒŒ ์ œ๊ณตํ•ด์ค„ ์ˆ˜ ์žˆ๋‹ค.

๐ŸŽจ UI Examples We Played With

๐Ÿฅค Pickup Order Status - From order placed โ†’ in progress โ†’ ready for pickup
live_activity_order.zip


โšพ Baseball Scoreboard - Real-time score updates with team logos and vibes
live_activity_score.zip


๐Ÿƒ Live Running Tracker - Distance, speed, step count โ€” all updating live!
live_activity_step.zip


๋งˆ๋ฌด๋ฆฌ

iOS์˜ WidgetKit+ActivityKit๋ฅผ ์‚ฌ์šฉํ•˜๋Š” Live Activity์— ๋Œ€ํ•ด์„œ ์ž์„ธํžˆ ๋‹ค๋ค„๋ณด์•˜๋‹ค.

Widget Extension์„ Flutter ํ”„๋กœ์ ํŠธ์— ์ถ”๊ฐ€ํ•˜๊ณ , Widget์˜ ๋นŒ๋“œ ๋ณ€ํ˜•์„ ์ ์šฉํ•˜์—ฌ Live Activity๋ฅผ Flutter์—์„œ ์–ด๋–ป๊ฒŒ ์ ์šฉํ•ด์•ผ ํ•˜๋Š”์ง€๋ฅผ ์•Œ์•„๋ณด์•˜๋‹ค.

ActivityKit์„ ์‚ฌ์šฉํ•ด Live Activity๋ฅผ ์‹œ์ž‘ํ•˜๊ณ , ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•˜๊ณ  ์ข…๋ฃŒํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด์„œ ์•Œ์•„๋ณด์•˜๊ณ , ๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ ์ „์šฉ ํ‘ธ์‹œ๋ฅผ ํ†ตํ•ด ์•ฑ ์™ธ๋ถ€์—์„œ๋„ ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๊ฑฐ๋‚˜ ์‹œ์ž‘ํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ์ ์šฉํ•ด ๋ณด์•˜๋‹ค.

๋ณธ๋ฌธ์—์„œ๋Š” ๋‹ค๋ฃจ์ง€ ์•Š์•˜์ง€๋งŒ, Method Channel ์‚ฌ์šฉ์‹œ Android์—๋Š” ์ง€์›ํ•˜์ง€ ์•Š๋Š” ๊ธฐ๋Šฅ์ด๋‹ค ๋ณด๋‹ˆ, ๋ณ„๋„๋กœ ๋นˆ ์ด๋ฒคํŠธ๋ฅผ ์—ฐ๊ฒฐํ•ด์ฃผ๊ฑฐ๋‚˜ ์•„๋‹ˆ๋ฉด ๋ถ„๊ธฐ ์ฒ˜๋ฆฌ๋ฅผ ํ•˜์—ฌ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๋„๋ก ์ฃผ์˜ํ•˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.

UI์ ์ธ ๋ถ€๋ถ„์€ ๋‚ด์šฉ์ด ๊ธธ์–ด์ ธ ๋‹ค๋ฃจ์ง€ ์•Š์•˜์ง€๋งŒ, ์–ด๋ ต์ง€ ์•Š์œผ๋‹ˆ ์›ํ•˜์‹œ๋Š” UI๋ฅผ ๊ตฌํ˜„ํ•˜์‹ค ์ˆ˜ ์žˆ์„์‹ค ๊ฒ๋‹ˆ๋‹ค.

์ถ”ํ›„ Widget Extension์˜ ๋˜ ํ•˜๋‚˜์˜ ํ•ต์‹ฌ ๊ธฐ๋Šฅ์ธ ํ™ˆ ์œ„์ ฏ๊ณผ ์ž ๊ธˆํ™”๋ฉด ์œ„์ ฏ์— ๋Œ€ํ•ด์„œ๋„ Flutter ํ”„๋กœ์ ํŠธ์— ์ ์šฉํ•˜๊ณ  ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด์„œ ๋‹ค๋ค„๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

๋ผ์ด๋ธŒ์—‘ํ‹ฐ๋น„ํ‹ฐ์— ๋Œ€ํ•ด ์ ์šฉ์ด ์•ˆ๋˜๊ฑฐ๋‚˜ ๊ถ๊ธˆํ•œ ๋ถ€๋ถ„ ์žˆ์œผ๋ฉด ๋Œ“๊ธ€ ๋‚จ๊ฒจ์ฃผ์„ธ์š” !

profile
Flutter Developer

0๊ฐœ์˜ ๋Œ“๊ธ€