본문 바로가기
👨‍💻 프로그래밍/🍎 iOS 개발

🍎 WidgetKit 이용해서 애플 위젯 개발하기

by 개발자 진개미 2024. 6. 22.
반응형

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인지에 관해 전반적으로 알려주고, 세부적인 설정을 할 수 있게 해 줍니다.
  • 이를 위해 Widgetvar body: some WidgetConfiguration이라는 변수를 가져야 하고, 이 안에서 원하는 WidgetConfigration을 골라야 합니다.
  • WidgetConfiguration은 크게 StaticConfiguration이 있고, IntentConfiguration이 있습니다. 
StaticConfigration 모든 유저에게 똑같은 위젯이라고 생각하면 됩니다. 아! 당연히 데이터는 다르죠. 근데 똑같은 UI라는 뜻입니다.
IntenetConfiguration 세부 설정이 가능한 위젯이라고 생각하면 됩니다. 유저가 원하는 정보를 설정해서 유저마다 원하는 방식으로 서로 다른 UI를 볼 수 있습니다.

 

IntentConfiguration은 위와 같이 추가적인 설정이 가능합니다.

 

  • 어떤 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 배열을 미리 제공

 

  • TimelineTimelineEntryTimelineReloadPolicy를 제공해야 합니다.
  • TimelineReloadPolicy는 3가지가 있습니다.
atEnd 특정 시간 이후에 업데이트 합니다.
never Timeline을 절대 업데이트하지 않습니다. 앞으로의 데이터를 미리 알 수 없고, 유저의 동작에 의해서 데이터가 변경될 경우 이렇게 합니다.
after 현재 TimelineEntry가 끝나면 업데이트 합니다.

HOWTO : 여러 사이즈에 따라 다른 View 보여주기

Widget은 여러 크기를 가질 수 있는데 이때 1가지 View만 제공한다면 그 사이즈가 단순히 확대되거나 텅 비어 있게 됩니다. 굉장히 이상해 보이죠? 사실 이렇게 하느니 큰 사이즈를 제공하지 않는게 낫습니다. (애플 공식 입장)

 
다른 크기를 제공하는건 굉장히 간단합니다. 우선 Widgetvar 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")

불러 올 때도 마찬가지로 suiteNameApp Group 이름을 넣어주고, 나머지는 원래 UserDefaults를 사용할 때와 똑같습니다.


HOWTO : 원래 앱에서 Widget에 데이터 업데이트 요청하기

  • WidgetKit은 마음대로 백그라운드에서 호출할 수 없고, Timeline으로 미래의 UI를 미리 제공해야 합니다.
  • 하지만 유저가 앱에서 상호작용을 해 데이터가 바뀌어 Widget의 내용도 업데이트 돼야 한다면 어떻게 할까요?
  • 이럴때는 WidgetKit의 함수를 호출해서 Widget을 업데이트 할 수 있습니다.

 

WidgetCenter.shared.reloadTimelines(ofKind: "ExampleWidget")

참고할 만한 자료들


반응형