📄 컨텐츠
연습용 프로젝트를 만들다가 위젯을 도입하게 되었다. 위젯에 대해서는 하나도 모르는 상태로 시작해서 하나하나 알게된 내용을 정리해보려 한다.
Widget
Creating a widget extension | Apple Developer Documentation
위젯에 대한 공식 문서를 살펴보면 위젯을 어떻게 추가하는지 어떻게 코드를 적어야하는지 예시가 자세하게 설명되어 있다. 그 예시 코드로 설명을 적어보겠다.
Widget 타겟 추가하기
위젯은 다른 일반 화면처럼 단순히 View 파일을 생서해서 적용하는 것이 아니라 별도의 타겟을 추가해야한다.
따라서 만약 기본 앱 타겟에서 사용하는 기능들을 위젯에서도 사용하고 싶다면 해당 공통 기능들은 따로 모듈을 분리해서 사용하면 좋다.
Widget 코드 작성하기
@main
struct GameStatusWidget: Widget {
var body: some WidgetConfiguration {
StaticConfiguration(
kind: "com.mygame.game-status",
provider: GameStatusProvider(),
) { entry in
GameStatusView(entry.gameStatus)
}
.configurationDisplayName("Game Status")
.description("Shows an overview of your game status")
.supportedFamilies([.systemSmall])
}
}
기본적으로 위젯의 App(UIKit으로 따지면 AppDelegate)에 해당하는 부분이다. 기본 앱 타겟이 App 프로토콜을 채택하고 있다면 위젯은 Widget 프로토콜을 채택하고 있다.
Configuration에서 받고있는 값을 크게 세가지가 있다. kind, provider, content 이렇게 세가지이다. 각각은 다음과 같다.
- kind: 위젯을 식별하는 문자열이다. 위젯이 어떤 것인지 설명을 담고 있어야한다.
- provider: TimelineProvider 을 채택하는 객체로 WidgetKit이 언제 위젯을 렌더링할지 알려주는 Timeline을 제공한다. Timeline은 커스텀 TimelineEntry로 이루어진 시퀀스이다. TimelineEntry는 어느 시점에 렌더링할지에 대한 시간과 렌더링될 때 위젯의 뷰가 필요한 속성들을 담고 있다.
- 즉
TimelineEntry
는 어느 시점에 어떤 내용을 위젯에서 표현할지를 담고있는 객체라고 생각하면 된다. - content: 이 부분은 TimelineProvider가 지정된 시점에서 제공해주는 TimelineEntry에 담겨있는 정보를 SwiftUI View에 담아서 위젯으로 렌더링해주는 부분이다.
Provider & Entry
// Entity
struct GameStatusEntry: TimelineEntry {
var date: Date
var gameStatus: String
}
//Provider
struct GameStatusProvider: TimelineProvider {
func getTimeline(in context: Context, completion: @escaping (Timeline<GameStatusEntry>) -> Void) {
// Create a timeline entry for "now."
let date = Date()
let entry = GameStatusEntry(
date: date,
gameStatus: gameStatusFromServer
)
// Create a date that's 15 minutes in the future.
let nextUpdateDate = Calendar.current.date(byAdding: .minute, value: 15, to: date)!
// Create the timeline with the entry and a reload policy with the date
// for the next update.
let timeline = Timeline(
entries:[entry],
policy: .after(nextUpdateDate)
)
// Call the completion to pass the timeline to WidgetKit.
completion(timeline)
}
}
TimelineProvider는 다음과 같이 작성된다. 위의 코드는 게임의 상태를 15분마다 업데이트해주는 Provider 코드이다. Entry는 업데이트 시점을 Date 타입으로 가지고 있고 그 시점에 View에 전달해야할 속성을 가지고 있다. nextUpdateDate
변수는 어느 시점에 다음 엔트리를 업데이트할지 정해주는 Date 타입의 변수이다.
Timeline을 생성해줄 때 쓰는 policy
라는 속성은 업데이트 시점 정책을 정해주는 속성이다. 종류는 atEnd, after, never 세가지가 있다.
atEnd는 타임라인이 끝나는 시점에 다시 타임라인을 업데이트하는 정책이고
after는 전달한 Date 타입 속성에 맞춰 타임라인을 업데이트,
마지막으로 never는 처음 타임라인을 불러온 이후 다시 타임라인을 업데이트 하지 않는다는 정책이다.
위의 코드에서는 after에 nextUpdateDate
를 전달해 15분 뒤에 타임라인을 업데이트하도록 설정했다.
Snapshot & PlaceHolder
struct GameStatusProvider: TimelineProvider {
var hasFetchedGameStatus: Bool
var gameStatusFromServer: String
func getSnapshot(in context: Context, completion: @escaping (Entry) -> Void) {
let date = Date()
let entry: GameStatusEntry
if context.isPreview && !hasFetchedGameStatus {
entry = GameStatusEntry(date: date, gameStatus: "—")
} else {
entry = GameStatusEntry(date: date, gameStatus: gameStatusFromServer)
}
completion(entry)
}
// ...
}
getSnapshot은 일종의 샘플 타임라인을 제공해주는 메서드이다. 이 샘플 타임라인은 어느 시점에 쓰일까? 바로 우리가 앱의 홈 화면에서 위젯을 추가하려고 할 때 제공되는 예시 위젯을 보여줄 때 사용된다.
아래의 이미지를 보면 위젯을 추가하려고 했을 때의 나오는 예시 위젯 이미지들이다.
저 샘플 이미지에 어떤 내용을 담을지는 getSnapshot
메서드를 통해 정의해 줄 수 있다.
그런데 찾아보니까 아래와 같이 placeholder
라고 불리는 메서드도 있었다. 느낌 상으로는 이 메서드가 예시 이미지에 더 어울리는 명칭의 메서드였다.
func placeholder(in context: Context)
-> Entry { }
placeholder
라고 하면 getSnapshot
과 비슷한 동작을 하게 될 것 같은데 어떤 때 쓰이는 것일까?
알고보니 placeholder
메서드는 바로 실제 위젯에서 Entry 값이 아직 로딩중일 때 위젯에 띄워주는 placeholder 내용을 정의해주는 메서드이다.
Widget View
struct GameStatusView : View {
@Environment(\.widgetFamily) var family: WidgetFamily
var gameStatus: GameStatus
var selectedCharacter: CharacterDetail
@ViewBuilder
var body: some View {
switch family {
case .systemSmall: GameTurnSummary(gameStatus)
default: GameDetailsNotAvailable()
}
}
}
View의 경우는 일반 SwiftUI 뷰와 같다. 위에서 알아야할 부분은 환경변수로 쓰이고 있는 family
이다. 위젯은 기본적으로 크기가 다양하다. 그렇기 때문에 크기에 따라 View 가 담을 수 있는 정보의 양도 바뀌고 형태도 바뀔 수 있다. 그래서 그 크기를 Enum으로 정의해놓은 것이 family
이다.
small | medium | large |
---|---|---|
![]() |
![]() |
![]() |
family
속성을 이용하면 위와 같이 사이즈 별 UI를 구성할 수 있다.
📍 레퍼런스
https://developer.apple.com/documentation/widgetkit/creating-a-widget-extension/
'iOS > Swift' 카테고리의 다른 글
some & any (0) | 2025.01.07 |
---|---|
Swift의 다양한 이미지 - UIImage, CGImage, CIImage (0) | 2025.01.03 |