WidgetKit이 뭐야
iOS, iPadOS, MacOS 등에 제공할
Widget을 개발할 수 있는
라이브러리
Extension?
WidgetKit은 사실 원래 앱에 Library 처럼 단순히 추가되는 추가 기능이 아닙니다. Extension이라는 형태로 추가되는데, 같은 앱이지만 앱과 상호작용할 수 있는 곳을 추가로 제공해 주는 개념이라고 생각하시면 됩니다. Widget 외에도 Siri나 Appclip 같이 앱을 실행하고 상호작용하는 형태 외의 여러 형태의 extension을 사용할 수 있습니다. 문제는 Extension은 애플의 정책상 Sandbox화 돼 있어 다른 앱 처럼 취급 돼 원래 앱의 여러 Class나 데이터를 접근할 수 없다는 겁니다. 이건 뒤에서 자세히 알아보겠습니다.
위젯은 앱이 아니다
애플은 몇 번이나 위젯은 작은 앱이 아니라고 강조합니다. 이건 단순히 디자인적인 측면만 말하는건 아니고, WidgetKit의 구조에서도 보실 수 있겠지만 WidgetKit은 마음대로 원래 앱의 함수를 호출하거나 네트워크 콜을 할 수 없습니다. Timeline이라는 단위를 사용해 미리 업데이트 될 데이터를 제공해야 하고, 유저가 Widget을 자주 볼 수록 자원 사용 기회는 많아집니다.
WidgetKit의 구조
Widget | 시작 지점, 여러 설정을 할 수 있음 |
WidgetConfiguration | Widget 종류 interface, StaticConfiguration, IntenetConfiguration 등이 있음 |
TimelineEntry | Widget을 그리기 위해 필요한 데이터들, date는 무조건 있어야 함 |
TimelineProvider | TimelineEntry를 얻기 위해 iOS에서 호출함. placeholder 상태, 현재 상태, 앞으로의 상태를 제공해야 함 |
TimelineProviderContext | Timeline을 제공할 때 쓸 수 있는 여러 데이터를 담고 있음. 현재 Widget의 크기, 보여지고 있는지 등의 정보가 있음. |
- 간단히 소개했듯, WidgetKit은 Extension으로 추가되기 때문에 엄밀히 말하면 다른 앱 입니다. 즉, 원래 앱에 있는 여러 파일들을 접근할 수 없고, 새로운 프로젝트인 것처럼 다뤄야 합니다.
WidgetKit의 구조 : Widget
@main
struct ExampleWidget: Widget {
let kind: String = "ExampleWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
ExampleWidgetEntryView(entry: entry)
.padding()
.background()
}
.configurationDisplayName("위젯의 이름이 여기에 들어가야 해요")
.description("위젯에 관한 설명은 여기에 들어가야 해요")
.supportedFamilies([.systemSmall, .systemMedium])
}
}
- 우선 원래 앱이 @main으로 마킹된 struct가 있고 이게 App 프로토콜을 상속했듯이, WidgetKit 또한 @main으로 마킹된 struct가 있고 이건 Widget 프로토콜을 상속합니다.
- Widget 프로코톨은 이 Widget이 어떤 Widget인지에 관해 전반적으로 알려주고, 세부적인 설정을 할 수 있게 해 줍니다.
- 이를 위해 Widget은 var body: some WidgetConfiguration이라는 변수를 가져야 하고, 이 안에서 원하는 WidgetConfigration을 골라야 합니다.
- WidgetConfiguration은 크게 StaticConfiguration이 있고, IntentConfiguration이 있습니다.
StaticConfigration | 모든 유저에게 똑같은 위젯이라고 생각하면 됩니다. 아! 당연히 데이터는 다르죠. 근데 똑같은 UI라는 뜻입니다. |
IntenetConfiguration | 세부 설정이 가능한 위젯이라고 생각하면 됩니다. 유저가 원하는 정보를 설정해서 유저마다 원하는 방식으로 서로 다른 UI를 볼 수 있습니다. |
- 어떤 WidgetConfiguration을 쓸 지 정하면, WidgetConfiguration에 modifer로 추가적인 설정들을 할 수 있습니다. 가장 많이 쓰이는 거 2개만 우선 설명 드리겠습니다.
configurationDisplayName | Widget을 추가할 때 보이는 제목, 아래 사진의 1번. |
description | Widget을 추가할 때 보이는 설명들, 아래 사진의 2번. |
supportedFamilies | Widget은 여러 크기를 가질 수 있는데 위젯이 가질 수 있는 크기를 모두 명시에 배열로 넘겨줄 수 있음. |
WidgetKit의 구조 : TimelineProvider, TimelineEntry
- WidgetKit은 배터리나 램을 앱이 과다 사용할 수 없게, 설계부터 자원을 마음대로 호출할 수 없게 했습니다.
- Widget을 만드는데 필요한게 뭘까요? UI와 데이터입니다.
- 그 중에서도 UI는 미리 정의해 놓고, 데이터에 따라 바뀝니다.
- 데이터는 시간에 따라 바뀝니다. (그렇지 않은 경우도 있지만)
- 이 시간에 따라 바뀌는 데이터를 WidgetKit에서는 Timeline이라는 걸로 추상화 했습니다.
- TimelineProvider를 시스템이 호출해 TimelineEntry를 얻어 TimelineEntry를 미리 만들어 둔 UI에 넘겨줘 Widget을 보여주는 형태입니다.
- TimelineEntry는 Widget을 만들 때 필요한 데이터를 자유롭게 정의할 수 있습니다. 단! 1가지 제약이 있는데 반드시 Date 타입의 date를 가져야 합니다.
- TimelineProvider는 3가지 함수를 구현해야 합니다.
placeholder | TimelineEntry가 없을 때 보여줄 더미 데이터를 제공 |
getSnapshot | 현재 상태의 TimelineEntry를 제공 |
getTimeline | 미래 상태의 TimelineEntry 배열을 미리 제공 |
- Timeline은 TimelineEntry와 TimelineReloadPolicy를 제공해야 합니다.
- TimelineReloadPolicy는 3가지가 있습니다.
atEnd | 특정 시간 이후에 업데이트 합니다. |
never | Timeline을 절대 업데이트하지 않습니다. 앞으로의 데이터를 미리 알 수 없고, 유저의 동작에 의해서 데이터가 변경될 경우 이렇게 합니다. |
after | 현재 TimelineEntry가 끝나면 업데이트 합니다. |
HOWTO : 여러 사이즈에 따라 다른 View 보여주기
Widget은 여러 크기를 가질 수 있는데 이때 1가지 View만 제공한다면 그 사이즈가 단순히 확대되거나 텅 비어 있게 됩니다. 굉장히 이상해 보이죠? 사실 이렇게 하느니 큰 사이즈를 제공하지 않는게 낫습니다. (애플 공식 입장)
다른 크기를 제공하는건 굉장히 간단합니다. 우선 Widget의 var body: some WidgetConfiguration에서 entry를 받아와 View를 호출하게 되는데, 이 때 entry를 전달해 주면서 widgetFamily 값을 @Environment로 받아와 분기를 치면 됩니다.
이렇게 말로 풀면 어렵지만 코드를 보면 간단합니다.
struct ExampleEntryView : View {
var entry: TimelineEntry
@Environment(\.widgetFamily) var family
var body: some View {
switch family {
case .systemSmall:
ExampleSmallView(entry)
case .systemMedium:
ExampleMediumView(entry)
default:
EmptyView()
}
}
}
HOWTO : 원래 앱과 Widget 사이 데이터 공유하기
- extension은 원래 앱과 파일, 데이터를 공유하지 않습니다.
- 그렇기 때문에 공유하고 싶은 데이터를 명시적으로 설정해서 받아와야 합니다.
- 데이터 공유를 위해서는 익숙한 UserDefaults를 사용합니다.
- 다만 standard를 사용하는건 아니고, App Group을 사용해 공유합니다.
1 - App Group 설정하기
원래 앱의 프로젝트의 설정에 가서 Signing & Capabilities에 가 줍니다.
Capability에서 App Groups를 추가해 줍니다.
App Group을 만들어 줍니다.
WidgetKit 설정에서 똑같은 걸 반복해 줍니다.
2 - 저장하고자 하는 데이터 원래 앱에서 UserDefaults에 저장하기
let userDefaults = UserDefaults(suiteName: "group.com.jinkyumpark.substrack")
userDefaults?.set(Date(), forKey: "lastUpdatedDate")
우선 UserDefaults의 suiteName에 1번에서 만든 App Group 이름을 넣어 주고, set을 사용해 원래 UserDefaults를 사용할 때와 똑같이 사용해 주시면 됩니다.
3 - 저장한 데이터 WidgetKit에서 UserDefaults에서 불러오기
let userDefaults = UserDefaults(suiteName: "group.com.ant.example")
let lastUpdatedDate = userDefaults?.string(forKey: "lastUpdatedDate")
불러 올 때도 마찬가지로 suiteName에 App Group 이름을 넣어주고, 나머지는 원래 UserDefaults를 사용할 때와 똑같습니다.
HOWTO : 원래 앱에서 Widget에 데이터 업데이트 요청하기
- WidgetKit은 마음대로 백그라운드에서 호출할 수 없고, Timeline으로 미래의 UI를 미리 제공해야 합니다.
- 하지만 유저가 앱에서 상호작용을 해 데이터가 바뀌어 Widget의 내용도 업데이트 돼야 한다면 어떻게 할까요?
- 이럴때는 WidgetKit의 함수를 호출해서 Widget을 업데이트 할 수 있습니다.
WidgetCenter.shared.reloadTimelines(ofKind: "ExampleWidget")
참고할 만한 자료들
- Apple의 HIG의 Widget에 대한 부분
- WWDC 2020의 Widget 소개 영상
'👨💻 프로그래밍 > 🍎 iOS 개발' 카테고리의 다른 글
🍎 iOS에서 서버를 거치지 않고 알림 보내기 (UserNotifications) (0) | 2024.08.02 |
---|---|
🍎 XCTest로 Swift에서 Unit Test 하기 (0) | 2024.07.28 |
🍎 SwiftUI에서 숫자 입력하면 실시간으로 구분자(,) 붙이기 (0) | 2024.07.21 |
🍎 Swift Data를 써서 iCloud 연동 기능 만들기 (1) | 2023.12.24 |
🍎 취미 iOS 앱 개발 후기 (0) | 2023.08.03 |