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? ์ด๊ฒ ๋ค ๋ญ๊น ?
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 ์ฌ์ฉํด ๋ณด๊ธฐ
๋ณธ๊ฒฉ์ ์ผ๋ก Live Activity๋ฅผ ์ฌ์ฉํ๊ธฐ ์์ Flutter์ ๋ฐฐํฌ๋ ํจํค์ง ์ค Live Activity ๊ธฐ๋ฅ์ Flutter์์ ๊ฐ๋จํ๊ฒ ๊ตฌํํ ์ ์๋ live_activities ํจํค์ง๊ฐ ์๋ค.
ํด๋น ํจํค์ง๋ฅผ ์ฌ์ฉํ๊ฒ ๋๋ฉด, Native๋ก ์ฒ๋ฆฌํด์ผ ํ๋ ์ด๋ฒคํธ ๋ฐ ๊ธฐ๋ฅ๋ค์ ์์ฝ๊ฒ Flutter์์ ์ฌ์ฉํ ์ ์๋๋ก ํด์ฃผ๋ ์ ์ ํธ๋ฆฌํ์ง๋ง ์ฌ์ ํ Extension์ด๋ UI ๋ถ๋ถ์ ์ง์ ๋ค์ดํฐ๋ธ์์ ์ฒ๋ฆฌ๋ฅผ ํด์ผํ๊ธฐ ๋๋ฌธ์ ์ง์ ๊ตฌ์ถํ๋ ๊ฒ์ ์ถ์ฒํฉ๋๋ค !
์์ ์ค๋ช ํ ๊ฒ๊ณผ ๊ฐ์ด Widget์ด๋ Live Activity, DynamicIsland๋ ๋ชจ๋ UI๋ SwiftUI๋ก ์์ ์ ํด์ผํ๋ค. ๊ฐ๋ฒผ์ด UI๋ง ์ฒ๋ฆฌํ๋ ๊ฒ์ SwiftUI๋ฅผ ๋ชจ๋ฅด๋๋ผ๋ ์กฐ๊ธ๋ง ์ฐพ์๋ณด๋ฉด์ ํ๋ฉด ์ด๋ ต์ง๋ ์์ ๊ฒ์ด๋ผ ์๊ฐํ๋ค.
์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก Flutter ํ๋ก์ ํธ์ ์ ์ฉํด๋ณด๋๋ก ํ์.
ํจํค์ง ์์๋ก ๋ณด๋ฉด ์ถ๊ตฌ ๊ฒฝ๊ธฐ ์ค์ฝ์ด๊ฐ ๋์ค๋ ์ค์๊ฐ ํํฉ์ ์ ๊ณตํ๊ณ ์๋๋ฐ, ์ฌ๊ธฐ์๋ ์์๋ก ์ผ๊ตฌ ๊ฒฝ๊ธฐ ์ ์๋ฅผ ๋ณด์ฌ์ฃผ๋ ๊ธฐ๋ฅ์ ๋ง๋ค์ด๋ณด๋ฉด์ Live Activity๋ฅผ ์ฌ์ฉํด ๋ณด๋๋ก ํ์.
ํ๋ก์ ํธ ์์ค์์ ํฐ๋ฏธ๋์ ์ด๊ณ , XCode๋ฅผ ์ด์ด์ฃผ๋๋ก ํ์. ๋ง์ผ .xcworkspaces ํ์ผ์ด ์๋ค๋ฉด ios ํด๋๋ฅผ XCode์์ ์ง์ ์ด์ด์ค๋ ๋๋ค.
open ios/Runner.xcworkspace
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 ํ๊ฒฝ์ ๋ฐ๋ฅธ ์ผ์ด์ค๋ค์ธ๋ฐ, ์ด ์ธ ์ผ์ด์ค๋ค์ ๋๊ธ์ด๋ ๋ฉ์ผ ์ฃผ์๋ฉด ์ต๋ํ ํด๊ฒฐํ ์ ์๋๋ก ๋์ ๋๋ฆฌ๊ฒ ์ต๋๋ค..
ํด๋น ์๋ฌ๋ ๊ธฐ๋ณธ์ผ๋ก ์ถ๊ฐ๋ Preview์ ์ฝ๋๊ฐ ํน์ ๋ฒ์ ์ด์์์๋ง ์ง์๋๊ธฐ ๋๋ฌธ์ด๋ค. ๋ฏธ๋ฆฌ๋ณด๊ธฐ(as: .content, contentState: ..)๋ iOS 17.2 ์ด์์์๋ง ์ง์๋๊ธฐ ๋๋ฌธ์ Result Builder ์์คํ ์ด ํด๋น ๋ธ๋ก์ ์ฌ์ฉํ ์ ์๋ API ๋ฒ์ ์ ๋ง์กฑํ์ง ๋ชปํด ์ปดํ์ผ ์๋ฌ๊ฐ ๋ฐ์ํ ๊ฒ์ด๋ค.
ํด๊ฒฐ ๋ฐฉ๋ฒ์ผ๋ก๋ LiveActivity ํ์ผ ์๋ ํ๋ฆฌ๋ทฐ๋ฅผ ์ฃผ์์ฒ๋ฆฌํ๊ฑฐ๋ ๋๋ ์๋จ์ ๋ฒ์ ์ ๋ช ์ํด์ฃผ๋ฉด ๋๋ค.
![]() |
![]() |
Swift ํ๋ก์ ํธ์์๋ ๋ฐ์ํ์ง ์์ง๋ง, ์ด์ํ๊ฒ Flutter ํ๋ก์ ํธ์์ ์ต์คํ ์ ์ ์ถ๊ฐํ๋ ๊ฒฝ์ฐ์๋ง ์ฃผ๋ก ๋ฐ์ํ๋ ์๋ฌ๋ก, XCode์์ ๋น๋ ํ๊ฒ ๊ฐ ์ํ ์์กด์ฑ(Circular Dependency) ๋ฌธ์ ๊ฐ ์๊ฒผ์ ๋ ๋ฐ์ํ๊ฒ ๋๋ ์๋ฌ์ด๋ค.
Build Phases์ ์คํ ์์์ ๋ฐ๋ผ, ์์กด์ฑ ํ๊ฒ์ด ๊ผฌ์ด๋ฉด์ ์ํ ์์กด์ฑ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ ๊ฒ์ด๊ธฐ ๋๋ฌธ์ ์์๋ฅผ ์ฌ๋ฐฐ์น ํด์ฃผ๋ฉด ๋๋ค.
์ ๊ฐ ์ฐพ์ ๋ฐฉ๋ฒ์ผ๋ก๋ "Embed Foundation Extensions"๊ฐ "Thin Binary"๋ณด๋ค ์ ํ๋์ด์ผ ํ๋ค๋ ๊ฒ์ด๋ค. ํ์ฅ์ ๋จผ์ ์ฒ๋ฆฌํ๊ณ ๋ฉ์ธ ๋ฐ์ด๋๋ฆฌ์ ๋ณํฉ๋์ด์ผ ์๋ฌ๊ฐ ๋ฐ์ํ์ง ์๋๋ค.
๋จ ๋ค๋ฅธ ์์๋ก ๊ผฌ์ผ ์๋ ์์ผ๋ Build phases ์์ ์ฌ๋ฐฐ์นํ์๋ฉด ๋๋ถ๋ถ ํด๊ฒฐ์ด ๊ฐ๋ฅํ๋ค.
๋ง์ง๋ง ์ผ์ด์ค๋ ๋ฐ๋ก Bundle Identifier ๋ฌธ์ ์ด๋ค. Flavors๋ฅผ ์ฌ์ฉํ์ง ์๊ณ ๋จ์ผ ํ๊ฒฝ๋ง ๊ตฌ์ถํ ๊ฒฝ์ฐ์๋ ๋ฐ์ํ์ง ์๊ณ ๋น๋ ๋ณํ์ผ๋ก ์ธํด์ ๋ฐ์ํ๋ ์๋ฌ ์ผ์ด์ค๋ค.
์ต์คํ ์ ์ ๋ถ๋ชจ Idnetifier์ ๊ณ ์ ๋๊ฒ ๋๋๋ฐ, ๋ถ๋ชจ Runner๊ฐ ๋ณํ๋ ์ํ์ด๋ฏ๋ก ์ต์คํ ์ ๋ ๋์ผํ๊ฒ ๋น๋๋ณํ์ ์ ์ฉํด์ฃผ๋ฉด ํด๊ฒฐ๋๋ค.
๋ง์ผ ๋น๋๋ณํ์ด ๋์ด์๋๋ฐ, ์๋ฌ๊ฐ ๋ฐ์ํ์ง ์์๋ค๋ฉด ์๋ง๋ production ์ฑ์ผ๋ก ์คํํ ๊ฒฝ์ฐ์ผ ๊ฒ์ด๋ค.
production์๋ suffix๋ฅผ ๋ถ์ด์ง ์๊ธฐ ๋๋ฌธ์, suffix๊ฐ ๋ถ๋ (dev, qa, stg...) ์ฑ์ผ๋ก ์คํํด๋ณด๋ฉด ์๋ฌ๊ฐ ๋ฐ์ํ๋ค.
์ต์คํ ์ ์ ํ์ธํด๋ณด๋ฉด ๋ถ๋ชจ Bundle Identifier์ ์์ฑํ ์ต์คํ ์ ๋ช ์ผ๋ก ๋ ๋ฆฝ๋ Bundle Identifier๋ฅผ ๊ฐ์ง๊ณ ์๋ ์์ ฏ์ด ์์ฑ๋ ๊ฒ์ ํ์ธํ ์ ์๋ค.
Flavors ์ ์ฉ์ ๋ฉ์ธ ํ๊ฒ์ ํ ๊ฒ๊ณผ ๋์ผํ๊ฒ ์ต์คํ ์ ์๋ suffix๋ฅผ ์ถ๊ฐํ์ฌ ๋น๋๋ณํ์ ๋ฐ๋ฅธ ๋ถ๋ชจ Bundle Identifier์ ์ฑํฌ๊ฐ ๋ง๋๋ก ํด์ฃผ์.
๋น๋ ๋ณํ์ด ์ด๋ฏธ ์ธํ ๋ ํ๋ก์ ํธ์ธ ๊ฒฝ์ฐ์ ๊ฐ๋ ์ด ์ด๋ ค์ฐ์๋ค๋ฉด ์๋ ๊ณต์ ๋ ๊ธ์ ์ฐธ๊ณ ํ์๊ธธ ๋ฐ๋๋ค.
[Flutter] Flavor ๋น๋ ๋ณํ
์ถ๊ฐ๋ก ์ต์คํ ์ ์ผ๋ก ์ธํ์ฌ 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๊ฐ ์ ์์ ์ผ๋ก ํ์ฑํ ๋๋์ง๊น์ง ํ์ธํด๋ณด์.
์ฐ์ ์ ์๋ ์ฝ๋๋ฅผ ์ถ๊ฐํ์ฌ ๋จผ์ ํ ์คํธ๋ฅผ ํด๋ณด๋๋ก ํ๊ฒ ๋ค.
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 ์ฌ์ฉ์ ์ํ ๊ธฐ๋ณธ์ ์ธ ์ธํ ์ ๋์ด๋ฌ๋ค. ์ด์ 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 ์ฐ๋ ๋ฐ ์ฌ์ฉ ๋ฐฉ๋ฒ์ ์๋ ๊ธ ์ฐธ๊ณ !!
๋ง์ง๋ง์ผ๋ก ๋ชจ๋ ํ๋ก์ฐ๊ฐ ๋๋๋ค๋ฉด, Live Activity์ DynamicIsland์ ๋นํ์ฑํ ๋ฐ LockScreen ์ ๊ฑฐ ๋ฑ์ ์ข ๋ฃํ๋ ๋ฐฉ๋ฒ์ด๋ค.
์ด ํ๋ฆ๋๋ก ๊ธฐ๋ฅ์ ์ดํด๋ณด๋ฉด์, ๋ง๋ค์ด๋ณด๋๋ก ํ์ !
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 ๊ตฌ์ฑ์ ์์ ํ๋ผ๊ณ ํ๋ค.
์์ ํ ์คํธ์์ ์ด๋ป๊ฒ 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๊ด๋ จ ํ์ผ๊ณผ ์ด๋ฒคํธ ๊ด๋ จ ํ์ผ์ ๊ธ ๋ง์ง๋ง์ ๊ณต์ ํด ๋๋๋ก ํ๊ฒ ์ต๋๋ค. ํ์ํ์๋ค๋ฉด ์ฐธ๊ณ ํ์ ๋ ๋ฉ๋๋ค.
๋ค์์ผ๋ก ๋ผ์ด๋ธ์ํฐ๋นํฐ ๊ธฐ๋ฅ์ ํต์ฌ์ด๋ผ๊ณ ํ ์ ์๋ ์ค์๊ฐ ๋ฐ์ดํฐ ์ ๋ฐ์ดํธ์ ๋ํด์ ์์๋ณด๋๋ก ํ์.
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์ผ๋ก ์ ์กํด๋ณด๋ฉด ์ํ๊ฐ ๋ณ๊ฒฝ๋ ๊ฒ์ ํ์ธํ ์ ์๋ค.
๋ผ์ด๋ธ์ํฐ๋นํฐ ์ ์ฉ ํธ์๋ฅผ ํ ์คํธํ ํ๊ฒฝ์ด ์๋ ๊ฒฝ์ฐ์๋ ๋์๋ณด๋์์๋ ๋ถ๊ฐ๋ฅํ๊ณ , ํฌ์คํธ๋งจ์ผ๋ก ๊ฐ๋จํ๊ฒ ํ ์คํธ๋ฅผ ํด๋ณผ ์ ์๋ค.
ํฌ์คํธ๋งจ์ ์ ์ํ ํ 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์ผ๋ก ๋ผ์ด๋ธ์ํฐ๋นํฐ ์ ์ฉ ํธ์๋ฅผ ๋ณด๋ด๊ณ , ๋ผ์ด๋ธ์ํฐ๋นํฐ๊ฐ ์ ๋ฐ์ดํธ ๋๋ ๊ฒ๊น์ง ๋ฌธ์ ์์ด ์ฑ๊ณตํ์ ๊ฒ์ด๋ผ ์๊ฐํ๋ค. ํฌ์คํธ๋งจ์ ํ ์คํธ ์ฉ์ด๊ธฐ ๋๋ฌธ์, ์ค์ ์๋ฒ์์ ์ ์ก๋๋ ๊ฒ๋ ๋ฐ๋์ ํ ์คํธ ํด๋ณด์ ์ผ ํฉ๋๋ค.
๋ง์ผ ์ ๋๋ก ์๋์ด ์๋์๋ ๋ถ๋ค์ ๋ค์ ๋ฐ๋ณตํด์ ํด๋ณด์๊ณ ๊ทธ๋๋ ์๋๋ค๋ฉด ๋ฌธ์ ๋จ๊ฒจ์ฃผ์ธ์. ์ต๋ํ ์์ธํ๊ฒ ๋จ๊ฒจ์ฃผ์ ์ผ ํด๊ฒฐ์ด ๊ฐ๋ฅํฉ๋๋ค !!
์ด์ด์ ๋ผ์ด๋ธ์ํฐ๋นํฐ์ ์ํ๋ฅผ ์ข
๋ฃ ๋ฐ ์ ๊ฑฐํ๋ ๋ฐฉ๋ฒ์ ๋ํด์ ์์๋ณด๋๋ก ํ์.
์ฌ๊ธฐ์๋ 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๋ฅผ ์ฌ์ฉํ์ง ์์ผ๋ฉด ๋๋ค.
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์ ์ข
๋ฅ๋ง๋ค ๊ณ ์ ํ ํ ํฐ์ธ ๊ฒ์ด๋ค.
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์ ์ฌ์ฉํ์ฌ ์ฌ์ฉ์์ ์์น ๋ณํ์ ๋ก์ปฌ์์ ์ ๋ฐ์ดํธํ์ฌ ์ค์๊ฐ ํํฉ์ ๋ณด์ฌ์ฃผ๋ ๋ผ์ด๋ธ์ํฐ๋นํฐ์ด๋ค.
![]() |
![]() ![]() |
์ด์ฒ๋ผ ๋ผ์ด๋ธ์ํฐ๋นํฐ๋ฅผ ์ฌ์ฉํ๋ฉด ์ฃผ์ด์ง ๊ตฌ์กฐ์์์ ์ผ๋ง๋ ์ง ์์ ๋กญ๊ฒ ์ปค์คํ ํ ์ ์๊ณ , ์ํ๋ ์ค์๊ฐ ์ ๋ณด๋ฅผ ๋น ๋ฅด๊ณ ๊ฐํธํ๊ฒ ์ ๊ณตํด์ค ์ ์๋ค.
๐ฅค 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 ํ๋ก์ ํธ์ ์ ์ฉํ๊ณ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์ ๋ํด์ ๋ค๋ค๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
๋ผ์ด๋ธ์ํฐ๋นํฐ์ ๋ํด ์ ์ฉ์ด ์๋๊ฑฐ๋ ๊ถ๊ธํ ๋ถ๋ถ ์์ผ๋ฉด ๋๊ธ ๋จ๊ฒจ์ฃผ์ธ์ !