SwiftUI-Widget
종류: Swift
개요
- 생각해보니까 이때까지 위젯을 만들어본 적이 없어서 이번 기회에 만들어보려고 한다!
위젯 시작하기
- 사용하고자 하는 앱에 target을 눌러 widgetExtension을 선택
- 위젯 이름을 선택하고 finish버튼 클릭
- 여기서 live activity는 Dynamic Island와 잠금화면에 앱 데이터를 표시, 빠른 인터랙션 제공하는 새로운 위젯이라고 보면 됨(배민 주문 현황이 잠금화면에서 보이는 그런 위젯)
위젯의 개념
위젯 동작 방식
- 위젯은 항상 떠야하는데 로딩화면이 보이는 것은 UX적으로 매우 안좋음
- 따라서 항상 보일 수 있도록 애플이 구현해놓음
- 우리는 위젯을 업데이트할 시간을 담은 배열(timeline)을 widgetKit에게 주면 widgetKit은 그걸 보고 그 시간에 나와야하는 view를 위젯에게 전송해줌
→ 즉, view는 이미 준비가 되어있고 그시간마다 이미 만들어진 뷰를 표출하는 것
WidgetBundle
- 어떤 타입의 위젯을 제공할 건지를 결정
- 위젯을 여러개 제공할 때 사용
- 현재 liveActivity도 포함했기에 기본 코드로 제공되는 것으로 보임
import WidgetKit
import SwiftUI
@main
struct MyWidgetBundle: WidgetBundle {
var body: some Widget {
MyWidget()
MyWidgetLiveActivity()
}
}
Widget 기본 구조
- widget
- 위젯 갤러리에서 어떻게 보일지와 보이는 화면
- configuration
- StaticConfiguration
- 사용자 구성 가능한 속성이 없는 위젯의 경우. 예를 들어, 일반적인 시장 정보를 표시하는 주식 시장 위젯이나 트렌드 헤드라인을 표시하는 뉴스 위젯 존재
- IntentConfiguration
- StaticConfiguration과 달리 사용자가 구성할수 있는 프로퍼티가 있는, 위젯 편집을 제공하는 방식
- 위젯편집 기능을 제공함
- StaticConfiguration
- Provider(AppIntentTimelineProvider/TimelineProvider)
- 데이터를 불러오기 전 임시 화면
- 샘플데이터 보여줄 때 화면
- 언제 업데이트시킬지 timeline 전달
- timelineEntry
- TimelineEntry 프로토콜을 만족
- date라는 변수 필수로 필요
코드로 보자!
Widget
위젯 갤러리에서 보이는 화면 구성
struct MyWidget: Widget {
let kind: String = "MyWidget"
var body: some WidgetConfiguration {
AppIntentConfiguration(
kind: kind,
intent: ConfigurationAppIntent.self,
provider: Provider()
) { entry in
MyWidgetEntryView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
}
.configurationDisplayName("써니의 위젯")
.description("매분마다 새로운 이모지와 퇴근시간을 알 수 있습니다.")
.supportedFamilies([.systemMedium, .systemSmall, .systemLarge])
}
}
WidgetConfigurationIntent
- StaticConfiguration에는 없고 IntentConfiguration에 존재
- 어떤 데이터를 담을 건지 결정
struct ConfigurationAppIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource = "Configuration"
static var description = IntentDescription("This is an example widget.")
@Parameter(title: "Current Emoji", default: "😃") // 위젯 편집 시 보이는 문구
var currentEmoji: String
// 만약에 커스텀 entity를 넣고 싶으면 AppEntity protocol을 만족해줘야 한다
}
Provider
struct Provider: AppIntentTimelineProvider {
// 데이터를 불러오기 전(getSnapshot)에 보여줄 placeholder
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), configuration: ConfigurationAppIntent())
}
// 위젯 갤러리에서 위젯을 고를 때 보이는 샘플 데이터를 보여줄때 해당 메소드 호출
// API를 통해서 데이터를 fetch하여 보여줄때 딜레이가 있는 경우 여기서 샘플 데이터를 하드코딩해서 보여주는 작업도 가능
// context.isPreview가 true인 경우 위젯 갤러리에 위젯이 표출되는 상태
func snapshot(for configuration: ConfigurationAppIntent, in context: Context) async -> SimpleEntry {
SimpleEntry(date: Date(), configuration: configuration)
}
// 홈화면에 있는 위젯을 언제 업데이트 시킬것인지 구현하는 부분, 위젯편집 후 홈에 돌아갔을 때에도 다시 불림
func timeline(for configuration: ConfigurationAppIntent, in context: Context) async -> Timeline<SimpleEntry> {
var entries: [SimpleEntry] = []
// 1분, 2분뒤 ... 로 entry 값으로 업데이트하라는 코드
let currentDate = Date()
for minuteOffset in 0 ..< 60 {
let entryDate = Calendar.current.date(byAdding: .minute, value: minuteOffset, to: currentDate)!
let entry = SimpleEntry(date: entryDate, configuration: configuration)
entries.append(entry)
}
return Timeline(entries: entries, policy: .atEnd)
}
}
Entry View
실제로 위젯에 보이는 화면
struct MyWidgetEntryView : View {
var entry: Provider.Entry // 위젯을 업데이트할 시기를 widgetKit에 알리는 역할
var body: some View {
VStack(spacing: 12 ) {
HStack {
Text(entry.configuration.favoriteEmoji)
Text(entry.date, style: .time)
.dynamicTypeSize(.medium)
Spacer()
}
Text(checkWorkStatus())
.font(.caption)
.frame(maxWidth: .infinity)
}
}
}
앱과 위젯간의 데이터 공유를 위한 사전작업
- 앱과 위젯은 같은 프로젝트 내에 있지만 다른 타겟이여서 userDefault에 넣어놔도 가져오지는 못한다
- 따라서 appGroup을 추가해주고 UserDefault 가져올 때 userdefault.standard가 아닌 UserDeafult(suiteName:)을 호출하여 사용할 것
extension UserDefaults {
static let groupUserDefault: UserDefaults = UserDefaults(suiteName: "group.sunny.widgetGroup")! // widget과 함께 쓰기 위해 app group용 userdefault 생성
}
Intent을 이용하여 위젯 만들기
- IntentConfiguration과 Intent를 GUI로 만든다는 거 외에는 다른점 x
- 그리고 프로토콜에 App을 빼면 동일
struct IntentWidget: Widget {
let kind: String = "IntentWidget"
var body: some WidgetConfiguration {
IntentConfiguration( // AppIntent어쩌고가 아님!
kind: kind,
intent: WidgetThemeIntent.self,
provider: IntentProvider()
) { entry in
IntentWidgetEntryView(entry: entry)
.containerBackground(.clear, for: .widget)
}
.configurationDisplayName("써니의 컬러위젯")
.description"이모지와 색상을 골라보세요!")
.supportedFamilies([.systemMedium, .systemSmall, .systemLarge])
}
}
struct IntentEntry: TimelineEntry {
let date: Date
let configuration: WidgetThemeIntent
}
struct IntentProvider: **IntentTimelineProvider** { // AppIntent어쩌고가 아님!
func placeholder(in context: Context) -> IntentEntry {
IntentEntry(date: Date(), configuration: WidgetThemeIntent())
}
...
}
struct IntentWidgetEntryView : View {
var entry: IntentEntry
...
}
예시
https://github.com/sunny5875/WidgetPractice
참고
[iOS - SwiftUI] 1. 위젯 Widget 사용 방법 개념 (WidgetKit, WidgetFamily)
[swift]Widget 둘러보고 간단히 만들어보기(1)
WidgetKit (2) - TimelineEntry / TimelineProvider / TimelineReloadPolicy