본문 바로가기
Swfit/WidgetKit

사용자 설정이 가능한 Widget 설정 방법 (iOS 17 under)

by GGShin 2024. 1. 24.

위젯에서 사용자 설정을 할 수 있다는 사실을 알고 계셨나요?

배경에 추가한 위젯을 꾹 터치했을 때 Edit Widget이라고 나온다면 사용자 설정을 지원하는 위젯입니다. 애플의 날씨앱 위젯도 feed로 받아 볼 지역을 수정할 수 있는 수정 기능이 있습니다.

 

 

Widget 사용자 설정 예시 영상

이렇게 사용자 설정을 할 수 있는 Widget을 만드는 방식은 iOS 17 이후와 이전이 많이 달라졌습니다. iOS 17 이후는 WidgetKit에서 모든 것을 설정하지만 이전에는 SiriKit에서 설정을 했었더라구요. 그러다보니 iOS 17보다 낮은 버전에서도 사용자 설정 위젯을 지원하려면 17 이상과 이하 두 가지를 위한 설정을 각각 해줘야 합니다.

 

배포되어 있는 앱 타겟 버전이 16.2이기도 하고 iOS 16 유저가 17 유저보다도 많은 것으로 알고 있기 때문에 예전 configuration도 추가해주게 되었습니다. (17과 관련된 설정 방법은 애플 문서나 구글에 잘 나와있지만 그 이하 버전에 대한 방식에 대해서 알아보도록 하겠습니다.)

 

1. SiriKit Intent Definition File 추가하기

Widget에 SiriKit Intent Definition File을 추가해줍니다. 이름은 원하는 대로 지어주면 됩니다. (개인적인 팁으로,, 이름 마지막에 굳이 'Intent'라고는 명시하지 않아도 될 것 같습니다. 저는 추가했었는데, 그렇게 하니까 나중에 자동으로 생성되는 intent 클래스 이름에도 suffix로 intent가 붙어서 000IntentIntent가 되어서 보기 별로 안좋더라구요. :o )

 

 

2. Intent 설정하기

 

이 Intent의 용도는 사용자가 어떤 항목들을 수정하도록 할 것인지 명시해주는 겁니다. 날씨 앱의 경우는 사용자의 지역을 선택할 수 있도록 해주었지만, 각 앱의 용도에 맞는 항목들을 추가해주면 되겠습니다.

 

일단 (1) Category는 View로 선택해주고, (2) Widget에서 사용할 Intent이므로 Widgets check box에 표시해줍니다.

 

(3) 사용자의 input을 받을 항목은 3번 Parameter 부분에 추가해주면 됩니다. 저는 사용자가 문구를 입력할 수 있게 하고 싶어서 quote라는 이름의 파라미터를 추가해주었습니다. 그리고 어떻게 보여지게 할 지 타이틀을 정해주고, Widget에서 수정할 수 있게 할 것이므로 Configurable check box에 틱 표시를 해줍니다. 그리고 Default 값은 어떤 값으로 할 것인지도 설정해주면 됩니다. 다른 항목들도 보고 필요하면 추가해주면 됩니다.

 

설정한 후 나중에 확인하면 아래 처럼 위젯 수정을 눌렀을 때 사용자 input을 받을 항목들이 잘 나오게 됩니다. 

 

 

3. Entry 설정하기

 

Entry란 위젯에 보여줄 정보 Model 이라고 생각하면 됩니다. TimelineEntry protocol을 conform해주면 됩니다. 만약 위젯에 이미지, 글귀를 표시하겠다면 해당 항목들을 변수로 설정해주면 됩니다.

 

struct WeatherEntry: TimelineEntry {
    public let date: Date
    let image: Data?
    var quote: String
}

 

그리고 date는 반드시 들어가야 합니다. Widget이 데이터를 업데이트 할 때 사용되기 때문입니다.

 

4. IntentTimelineProvider 설정하기

이제 아마도 가장 손이 많이 갈 TimelineProvider만 설정해주면 됩니다. 

 

위에서 Intent를 설정하여 어떤 사용자 input을 받을 것인지 명시해주었습니다. 그러면 이제는 사용자가 무언가를 입력했을 때, 처리해서 위젯을 업데이트할 수 있도록 TimelineProvider를 설정해주어야 합니다. Widget은 widget gallery에 표시할 정보가 필요할 때, 업데이트가 되어야 할 때 등 특정 순간에 데이터를 요청하게 됩니다. 이 때 각 순간에 맞는 어떠한 정보를 줄 것인지 명시해 주는 것이 TimelineProvider의 역할입니다. 이 TimelineProvider는 종류가 몇 가지 있는데, SiriKit을 이용할 때 dynamic한 정보를 주려면 IntentTimelineProvider를 사용해야 합니다.

 

struct SiriKitIntentProvider: IntentTimelineProvider {

    typealias Entry = WeatherEntry
    typealias Intent = WeatherSiriIntent
    
    func placeholder(in context: Context) -> WeatherEntry {//...}
    
    func getSnapshot(for configuration: WeatherSiriIntent, in context: Context, completion: @escaping (WeatherEntry) -> Void) {//...}
    
    func getTimeline(for configuration: WeatherSiriIntent, in context: Context, completion: @escaping (Timeline<WeatherEntry>) -> Void) {//...}
   
}

 

struct를 하나 생성해주는데, IntentTimelineProvider를 conform하도록 합니다. 기본적으로 위에 나온 메서드들을 구현하도록 되어있습니다. 위에서 언급한 것처럼, Widget이 "정보 좀 줘" 라고 할 때 어떤 정보를 줄 지 작성한다고 생각하면 됩니다. 보면 placeholder로 사용될 정보, snapshot으로 사용될 정보, timeline으로 사용될 정보를 줘야 한다는 것을 알 수 있습니다. 

 

이 부분에 만일 서버에서 데이터를 받아 올 필요가 있다면 network call을 사용해 준다거나, 기존에 앱에 화면에 정보를 주듯이 동일하게 생각하고 구현해주면 됩니다. 사용자 input 정보는 configuration parameter에 있으므로 해당 항목도 업데이트에 이용해 주어야겠습니다. 

 

completion에 들어가는 타입을 보면 TimelineEntry 타입입니다. 화면에 보여질 내용들을 completion으로 전달해주면 Widget이 정보를 받아서 화면에 그려준 다는 것을 유추할 수 있습니다.

 

5. Widget 객체 body에 configuration 설정해주기

이제 모든 설정이 끝났고 Widget에게 "이거 사용하면 돼"라고 알려주기만 하면 됩니다. Widget을 conform하고 있는 객체를 보면 View처럼 body 변수가 있습니다. body의 타입은 some WidgetConfiguration인데, IntentConfiguration이라는 struct가 바로 이 타입에 해당됩니다. 

 

public var body: some WidgetConfiguration {
        self.makeWidgetConfiguration()
    }
    
    func makeWidgetConfiguration() -> some WidgetConfiguration {
        
#if os(iOS)
        if #available(iOS 17.0, *) {
     //...
            
        } else {
            return IntentConfiguration(kind: "WeatherWidget",
                                       intent: WeatherSiriIntent.self,
                                       provider: SiriKitIntentProvider()) { entry in
                WeatherEntryView(entry: entry)
            }.supportedFamilies(supportedFamilies)
        }
#endif
    }
    
    private var supportedFamilies: [WidgetFamily] {
        #if os(iOS)
        [.systemSmall, .systemMedium]
        #endif
    }

 

iOS 17를 기준으로 구현방식이 달라지므로 #available을 이용해서 나누어줍니다. 지금 구현한 방식은 iOS 17 미만을 대상으로 한 방식이므로 else 문 안에 작성해줍니다. IntentConfiguration은 총 세 가지 parameter가 있습니다. kind에는 intent configuration의 종류를 명시해주면 되고, intent는 사용될 intent의 타입을, provider에는 TimelineProvider 객체를 넣어줍니다.

 

그리고 entry 정보를 갖는 클로져 안에 entry를 widget view의 initializer에 넘겨주게 되는데, 한 번 위젯 화면을 나타내는 객체는 어떻게 생겼는 지 간단하게 보도록 하겠습니다.

 

struct WeatherEntryView: View {
    var entry: WeatherEntry // entry
    
    @Environment(\.widgetFamily) var family
    
    var body: some View {
        //...
    }
}

 

보면 EntryView는 View type이고 TimelineEntry타입의 변수를 멤버로 가지고 있습니다. 왜냐면, entry에 담긴 정보들을 위젯 화면에 보여주어야 하기 때문입니다. 

 

또 하나 보이는 widgetFamily는 widget의 디자인 종류입니다. WidgetFamily는 enum이기 때문에 switch문을 이용해서, widget 디자인별로 어떻게 화면을 구성할 것인지 구현해주면 됩니다. 모든 widget 디자인을 다 구현할 필요는 없습니다. 예를 들어서 지원하고자 하는 디자인이 .systemSmall과 .systemMideum만 이라면 두 종류만 UI를 구현해 주면 되고 나머지는 default로 빼서 간단한 Text view 정도만 넣어주어도 됩니다. 대신에 직전 코드 예시에 나온 makeWidgetConfiguration() 메서드에서 처럼 지원할 widget family 종류가 어떤 것인지 반드시 명시해줘야 다른 widget들이 widget gallery에 나오지 않게 됩니다. 

 

이렇게 하면 사용자 수정이 가능한 위젯 설정이 완료 됩니다! 👍🏼


참고자료

https://developer.apple.com/documentation/widgetkit/making-a-configurable-widget

 

Making a configurable widget | Apple Developer Documentation

Give people the option to customize their widgets by adding a custom app intent to your project.

developer.apple.com

https://swiftsenpai.com/development/configurable-widgets-static-options/

 

How to Create Configurable Widgets With Static Options? - Swift Senpai

A step-by-step guide with sample code to show you how to create a configurable widget with static options using Swift

swiftsenpai.com

 

반응형