개발

iOS Widget 구성을 위한 Widget Extension 기본부분 살펴보기

소소ing 2020. 11. 16. 18:16
반응형

해당 글에서 코드 부분은 파란색으로 표기 됩니다.

해당 글에서 중요 부분은 붉은색으로 표기 됩니다.

 

iOS14 에서 새롭게 추가된 Widget을 구성하려면 Widget Extension을 구성하여야 한다.

 

새롭게 추가된 Widget의 구성(Configuration)은 아래와 같다

- Provider : Widget의 화면 갱신을 담당

- View content : Widget의 화면을 담당

 

 

(Widget init)

기본적으로 Widget Extension을 생성하면 자동으로 생성되는 코드에서처럼 다음과 같이 Provider 및 View content를 선언된것을 볼 수 있다.

Ex)

// @main 부분은 위젯의 시작 지점으로 생각하면 편리하다.

@main

struct testWidget2: Widget {

    let kind: String = "testWidget2"

 

    var body: some WidgetConfiguration {

        // provider 부분에 구성된 Provider를 정의

        StaticConfiguration(kind: kind, provider: Provider()) { entry in

            // Widget에 보여질 화면을 정의

            testWidget2EntryView(entry: entry)

        }

        .configurationDisplayName("My Widget")

        .description("This is an example widget.")

    }

}

 

(Part Provider)

Provider에서는 Timeline entry를 포함하게 되며, Timeline을 이용하여 위젯의 화면을 갱신 시키도록 요청할 수 있다. 

Timeline은 TimeLineEntry로 구성된 배열과 갱신에 대한 정책을 파라미터로 가진다. 

Ex)

// policy에는 아래와 같이 기본적으로 3가지가 존재한다. 

// - atEnd: TimelineReloadPolicy : 시간 표시줄의 마지막 날짜가 지난 후 WidgetKit에서 새 시간 표시 막대를 요청하도록 지정하는 정책

// - never: TimelineReloadPolicy : 새 시간 표시줄이 사용 가능할 때 앱에서 WidgetKit 메시지를 표시하도록 지정하는 정책

// - after(_ date: Date) -> TimelineReloadPolicy : WidgetKit가 새 시간 표시 막대를 요청할 미래 날짜를 지정하는 정책

let timeline = Timeline(entries: entries, policy: .atEnd)

 

TimeLineEntry는 하나의 모델(구조체)로 필수적으로 date 값을 가져야하며, 만약 위젯에 다른 값을 추가로 필요로 한다면 date값 외에 추가 요소를 구성하여 구조체로 정의할 수 있다. 

Ex)

struct SimpleEntry: TimelineEntry {

    // 필수 요소

    let date: Date

 

    // 필요시 요소 추가 가능    // ... 생략 ...

}

 

 

[Snapshot]

그럼 Provider에서 포함되는 Snapshot은 어떻게 이용되는 것일까?

WidgetKit에서는 Snapshot을 'A single immediate snapshot, representing the widget’s current state.' 로 정의해 놓고 있다. 즉, 위젯의 현재 상태를 나타내는 즉각적인 이미지의 의미로 보면 될 듯하다.

 

Widget의 동작방식을 쉽게 알아보면 사용자가 위젯을 추가할때 getSnapshot(in:completion:) 메소드가 호출이 되고, 그 다음부터는

getTimeline(in:completion:) 메소드가 호출되는 형태로 동작된다.

 

Apple에서는 get함수에 대해 아래와 같이 이야기 한다. 

WidgetKit은 위젯이 일시적인 상황에서 나타날 때`getSnapshot (in : completion :)`을 호출한다.

'context.isPreview'가 'true'이면 위젯 갤러리에 위젯이 표시된다.

이 경우 가능한 한 빨리`completion` 핸들러를 호출하고 위젯의 현재 상태를 가져 오거나 계산하는 데 몇 초 이상 걸릴 수있는 경우 샘플 데이터를 제공 할 수 있다고 설명한다.

getSnapshot (in : completion :) 함수가 호출되는 부분은 내가 만든 앱에서 위젯 갤러리를 들어가서 사이즈별 위젯이 노출될때 호출된다.

ex) 

struct Provider: TimelineProvider {

    // ... 생략 ...

    // 샘플 프로젝트에서는 날짜를 표기하는 단순 위젯으로 현재 시간 정보를 샘플로 보여주도록 처리

    func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {

        let entry = SimpleEntry(date: Date())

        completion(entry)

    }

    // ... 생략 ...

}

 

 

[getTimeline and TimelineReloadPolicy]

그 다음으로 알아볼 getTimeline(in:completion:) 함수의 경우는 위젯이 설치 된 후 호출이 되며, 설치한 위젯의 화면 갱신에 대한 타임아웃을 정의하는 용도로 사용된다. 

getTimeline(in:completion:) 함수의 동작을 알기 위해서는 위에서 간략히 언급한 policy(TimelineReloadPolicy) 부분을 살펴볼 필요가 있다!

 

 

TimelineReloadPolicy에는 3가지 정책으로 구성되어 있다. 

- atEndTimelineReloadPolicy

시간 표시줄의 마지막 날짜가 지난 후 WidgetKit에서 새 시간 표시 막대를 요청하도록 지정하는 정책

Apple 예시 프로젝트를 보면서 살펴보자!

Ex)

struct Provider: TimelineProvider {

    // ... 생략 ...

    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {

        // TimeLineEntry로 사용할 빈 객체를 생성

        var entries: [SimpleEntry] = []

 

        // 날짜 정보를 받아서 1시간 간격으로 위젯을 변경하고 5시간 간격으로 Timeline을 새로 수정한다. 

        // - 여기서 Timeline 수정이란 위젯 정보를 변경하는 방식(?)에 대한 변경이라고 보면 이해하기가 쉽다.

        let currentDate = Date()

        for hourOffset in 0 ..< 5 {

            let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!

            let entry = SimpleEntry(date: entryDate)

            entries.append(entry)

        }

 

        // policy를 .atEnd 기반으로 동작하므로 entries 마지막 date가 지난 후 새로운Timeline으로 정책을 변경

        let timeline = Timeline(entries: entries, policy: .atEnd)

        completion(timeline)

    }

// ... 생략 ...

}

 

- neverTimelineReloadPolicy

새 시간 표시줄이 사용 가능할 때 앱에서 WidgetKit 메시지를 표시하도록 지정하는 정책

never 정책은 더이상 위젯의 화면을 변경하지 않고자 할 경우 Timeline 정책을 .never로 변경하여 사용이 가능하다. 

never로 변경한 정책은 WidgetCenter를 이용하여 위젯을 갱신 시켜 줄 수 있다. 

Ex)

import WidgetKit

// 아래 두 함수를 통해서 위젯 갱신을 수동으로 해줄 수 있다. 

// - 예를 들어 앱을 실행할 경우에만 위젯을 갱신하고자 할때 사용이 가능하다.

WidgetCenter.shared.reloadAllTimelines()

WidgetCenter.shared.reloadTimelines(ofKind:<Custom Name>)

 

 

- after(_ date: Date) -> TimelineReloadPolicy

WidgetKit가 새 시간 표시 막대를 요청할 미래 날짜를 지정하는 정책으로 기본적인 매커니즘은 atEnd와 유사하지만 policy 지정 부분에서 위젯이 갱신될 시간을 지정해 주는 부분이 다르다. 

Ex)

struct Provider: TimelineProvider {

    // ... 생략 ...

    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {

        // TimeLineEntry로 사용할 빈 객체를 생성

        var entries: [SimpleEntry] = []

 

        let currentDate = Date()

        let entry = SimpleEntry(date: currentDate)

        entries.append(entry)

 

        // policy를 .after 기반으로 동작하므로 after에서 지정한 시간에 새로운Timeline으로 정책을 변경

        let entryDate = Calendar.current.date(byAdding: .hour, value: 1, to: currentDate)!

        let timeline = Timeline(entries: entries, policy: .after(entryDate))

        completion(timeline)

    }

// ... 생략 ...

}

 

(How to Use?)

앞에서 Widget 구성을 위한 기본적인 구조를 살펴 보았다.

그럼 가장 기본적인 위젯 구성을 위해서는 어떤식으로 동작이 되는지 정리해 보자!

1. 위젯 설정 및 위젯 갤러리 화면 표시 설정

// Widget Code 시작 시점

@main

struct testWidget2: Widget { // : Widget 으로 정의 시 단일 위젯을 뜻한다. 여러 위젯 구성시에는 추후에 별도 글로 작성

    // "testWidget2" : 위젯 이름 지정 --> 해당 값은 WidgetCenter에서 위젯 식별 용도로 사용된다. (한 앱에 여러 위젯 구성 시 필요)

    let kind: String = "testWidget2"

 

    // 위젯 설정 구성

    var body: some WidgetConfiguration {

        // 위젯 구성시 이름과 Provider를 구성한다. 

        // - Provider는 SnapShot과 Timeline Entry로 구성된다.

        StaticConfiguration(kind: kind, provider: Provider()) { entry in

            // 위젯 화면 UI를 구성하는 부분으로 UI 구성은 testWidget2EntryView struct에서 구성한다.

            testWidget2EntryView(entry: entry)

        }

        // 위젯 이름

        .configurationDisplayName("My Widget")

        // 위젯 설명 지정

        .description("This is an example widget.")

        // 위젯 사이즈 지정 (해당 라인을 입력하지 않으면 3가지 사이즈를 모두 지원하게 된다.)

        .supportedFamilies([.systemSmall])

    }

}

 

 

2. TimelineProvider 구성하기

StaticConfiguration으로 Provider를 지정해 주어야 하기 때문에 Provider를 구성해야 한다. 

Provider는 3가지 함수를 포함한다. 

// TimelineProvider를 상속하여 Provider 재구성 통하여 Custom 하게 설정이 가능하다

struct Provider: TimelineProvider {

    func placeholder(in context: Context) -> SimpleEntry {

        SimpleEntry(date: Date())

    }

 

    // 위젯 갤러리 화면 진입 시 호출되는 미리보기 화면

    func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {

        let entry = SimpleEntry(date: Date())

        completion(entry)

    }

    

    // 위젯 설치 후 Timeline 정책에 맞춰 화면 갱신을 위해 호출된다. 

    // - 서버로 부터 데이터를 받아서 화면을 갱신해야 할 경우, 이곳에서 데이터 통신 및 화면 UI를 변경해 주어야 한다. 

    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {

        // Timeline Entry를 담을 객체를 생성한다. 

        // Timeline Entry를 예제 코드에서는 SimpleEntry로 구성을 했다. 

        // - Timeline Entry는 위젯의 화면에 사용할 요소들과 Date 요소를 구성하여 화면 갱신에 필요한 정보 및 화면 갱신 시점을 담아 두는 역활을 한다.

        var entries: [SimpleEntry] = []

 

        // 여기서는 단순히 날짜를 표시하기 위해 SimpleEntry에 Date 요소만 구성되어 있다.

        let currentDate = Date()

        for hourOffset in 0 ..< 5 {

            let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!

            let entry = SimpleEntry(date: entryDate)

            entries.append(entry)

        }

 

        let timeline = Timeline(entries: entries, policy: .atEnd)

        completion(timeline)

    }

}

 

 

3. Timeline Entry 구성하기

위젯이 설치되고 getTimeline를 이용하여 위젯이 갱신될때 위젯의 화면을 변경하려면 변경에 필요한 요소가 필요하게 된다. 

이때 해당 요소를 저장하는 용도로 사용하는 것이 Timeline Entry에 해당된다. 

또한, 변경에 대한 Timeline 기점이 되는 date 요소를 가지고 있기도 하다.

// 필수 요소인 Date 요소만 포함한 TimelineEntry

struct SimpleEntry: TimelineEntry {

    let date: Date

}

 

 

4. 위젯 화면 그리기

위젯을 구성하고 Provider를 등록하여 Timeline을 통해 위젯을 갱신하도록 하려면 실제 위젯을 그려야 한다. 

// View를 통하여 TimelineEntry에서 데이터를 가져와서 화면에 그려준다. 

struct testWidget2EntryView : View {

    var entry: Provider.Entry

 

    var body: some View {

        Text(entry.date, style: .time)

    }

}

 

 

 

[구성화면]

 

[참고사항]

- 위젯 화면에서는 버튼을 구성할 수 없다.

- 위젯 Timeline 갱신 시간을 몇초, 몇분, 몇시간 단위로 지정하더라도 정확하게 변경되는것을 보장하지 않는다. (OS에서 관리)

- 위젯 클릭 시 기본적인 액션을 앱 실행이다. (다른 액션을 위해서는 추후 살펴보자)

 

 

 

iOS Widget Extension에서 Button 액션과 같이 특정 영역에 다른 동작을 주려면 아래 글을 참고

 

iOS Widget Extension 각 항목별 터치 이벤트 구성하기

해당 글에서 코드 부분은 파란색으로 표기 됩니다. 해당 글에서 중요 부분은 붉은색으로 표기 됩니다. iOS Widget Extension에 대한 기본 정보는 아래 링크에서 확인이 가능하다. Widget Extension에 대한

sosoingkr.tistory.com

 

iOS Widget Extension에서 URL 이미지를 로딩하고 싶을 경우 아래 글을 참고하자

 

iOS Widget Extension에서 URL 이미지 로딩 방법!?

해당 글에서 코드 부분은 파란색으로 표기 됩니다. 해당 글에서 중요 부분은 붉은색으로 표기 됩니다. iOS Widget Extension에 대한 기본 정보는 아래 링크에서 확인이 가능하다. Widget Extension에 대한

sosoingkr.tistory.com

 

반응형