Swift Widget(위젯) 알아보기

마이노·2024년 2월 28일
4

15주 글쓰기 🐣

목록 보기
2/9
post-thumbnail

Widget

위젯을 사용하는 방법을 알아봅시다!

애플 공식문서에 따라 기존 앱에 위젯을 확장하는 방법을 차근차근 따라가 보도록 할게요.

Creating a widget extension | Apple Developer Documentation

간단한 사용법

  1. File → New → Target

  2. Widget Extension 생성
    Live Activity (Dynamic island) 여부와, Intent 구성(기초 설정)을 포함할 것인지도 설정 가능합니다.
    scheme 또한 추가 가능합니다. 이번 포스팅에서는 추가하도록 할게요.

  3. 위젯 확인

검색다양한 크기 별 위젯

앱의 위젯이 위젯 갤러리에 표시되려면 앱을 설치한 후 위젯이 포함된 앱을 한 번 이상 실행해야 합니다.

앱을 실행을 최소 한 번을 해야 앱이 검색어에 뜨게 됩니다. 해당 앱에 들어가 위젯을 추가해주면 간단하게 위젯을 만나볼 수 있습니다.

그렇다면 디테일한 설정은 어떻게 해야할까요? 위젯폴더를 살펴보며 알아보겠습니다.

폴더 톺아보기


다음과 같이 파일이 만들어지는데요, 하나하나 알아가보도록 하겠습니다.

  1. Bundle

    Widget 프로토콜을 채택한 구조체를 넣어줄 수 있습니다. 여러 유형의 위젯을 지원하려면 여기에 속성을 추가해줄 수 있습니다.

    공식문서에서는 다음과 같이 설명하고 있어요. 게임 앱을 예시로 들고 있습니다.

    게임에는 게임에 대한 요약 정보를 표시하는 위젯 하나와 개별 캐릭터에 대한 디테일한 정보를 표시하는 두 번째 위젯이 WidgetBundle에 있을 수 있습니다.

        @main
        struct GameWidgets: WidgetBundle {
           var body: some Widget {
               GameStatusWidget()
               CharacterDetailWidget()
           }
        }
  1. MyWidget
    해당 파일에 들어가보시면 다양한 구조체들이 추가가 되어있습니다.
        struct Provider: IntentTimelineProvider {}
        struct SimpleEntry: TimelineEntry {}
        struct MyWidgetEntryView: View {}
        struct MyWidget: Widget {}

이렇게 헷갈리는 이름들로 널부러져있는 코드들을 하나하나 알아보겠습니다.

Provider

위젯의 콘텐츠를 업데이트할 날짜와 시간을 지정하고 위젯을 렌더링하는 데 필요한 데이터를 포함합니다.

세가지의 함수가 존재합니다.

  • placeholder
func placeholder(in context: Context) -> SimpleEntry {
	SimpleEntry(date: Date(), configuration: ConfigurationIntent())
}

위젯이 데이터를 받기 전 기본적으로 화면에 보여주어야 할 값들을 설정하는 함수입니다. 현재시간에 대한 date와 configuration을 생성해 보여주고 있는 모습입니다.

  • getSnapshot
func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {
	let entry = SimpleEntry(date: Date(), configuration: configuration)
	completion(entry)
}

위젯을 고를 때 미리보기에서 보여지는 데이터들을 설정하는 단계입니다. 이미 나타난 경우 completion을 호출하고 위젯의 현재 상태를 가져오거나 오래걸릴 경우 샘플데이터를 제공해 보여줄 수도 있습니다.

  • getTimeline
func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
	var entries: [SimpleEntry] = []
	let currentDate = Date()
            
	for hourOffset in 0 ..< 4 {
    
	let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
	let entry = SimpleEntry(date: entryDate, configuration: configuration)
	entries.append(entry)
    
	}
    
	let timeline = Timeline(entries: entries, policy: .atEnd)
	completion(timeline)
}

현재 시간과 선택적으로 위젯을 업데이트 할 때 미래의 시간에 대한 타임라인 항목 배열을 제공합니다. 업데이트를 언제 진행할 것인지에 대한 정보를 담고 있는 메서드입니다.

현재의 코드는 0에서 3을 포함한 반복횟수 만큼의 시간을 entry에 담고 이를 Timeline으로 만들어 업데이트를 진행하는 코드입니다.

즉각적으로 반응하는 위젯을 만들고 싶습니다만 그럴 수 없어 보입니다. 그 이유는 무엇일까요?


위젯이 화면에 표시되어 있어도 위젯은 계속 활성화 되지 않습니다. 정해진 예산 내에서의 리로딩 계획이 필수적으로 보입니다. 위젯을 다시 로드하면 시스템 리소스가 소모되고 네트워크 처리가 추가적으로 들어가 있다면 배터리 소모가 발생하게 됩니다. 따라서 이러한 성능 영향을 줄이고 배터리 수명을 하루 종일 유지하기 위해서는 요청하는 업데이트의 빈도와 횟수를 제한해야 합니다.

WidgetKit은 시스템 로드를 관리하기 위해 정해진 예산을 사용하여 하루 동안 위젯을 다시 로딩하고 배포하는 과정을 거칩니다. 이 예산이 얼마나 주어질지는 동적으로 계산되며, 다음을 포함하는 다양한 요소를 고려해서 할당하게 됩니다.

  • 위젯이 사용자에게 표시되는 빈도와 시간
  • 위젯을 마지막 새로고침한 시간
  • 위젯이 포함된 앱이 활성 상태인지

할당된 위젯의 예산은 24시간 동안만 적용됩니다. 또한 사용자의 일일 사용 패턴에 맞게 조절하게 되는데요, 그렇다고 해서 예산이 정확히 자정에 초기화되는 것은 아닙니다. 애플에 따르면 사용자가 자주 보는 위젯의 경우 일일예산에는 보통 40~70회의 새로고침이 포함된다고 합니다. 대략적으로 15분에서 60분마다 위젯을 다시 새로고침하는 것으로 해석이 되지만 여러 요인으로 인해 간격이 달라집니다.

시스템이 사용자의 행동을 학습하는데에는 며칠이 걸리게 됩니다. 이 학습 기간 동안 위젯은 평소보다 더 많은 새로고침을 진행할 수 있습니다.

다시 코드로 돌아와서..

func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
	var entries: [SimpleEntry] = []
	let currentDate = Date()
            
	for hourOffset in 0 ..< 4 {
		let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
		let entry = SimpleEntry(date: entryDate, configuration: configuration)
		entries.append(entry)
	}
	let timeline = Timeline(entries: entries, policy: .atEnd)
	completion(timeline)
}

하나하나 살펴보겠습니다.

for hourOffset in 0 ..< 4 {
}

반복문은 0부터 3을 포함한다고 했습니다.

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

다음 코드를 통해 현재시간에 value만큼의 시간을 더한 값을 (0~3)
(현재시간이 오후2시면은, 2시,3시,4시,5시가 되겠네요!)

let entry = SimpleEntry(date: entryDate, configuration: configuration)
entries.append(entry)

저의 Entry 객체에 넣어준 후 배열에 append시켜줍니다.

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

이후 entires 배열을 통해 Timeline을 만들고 completion을 통해 호출합니다.
.atEnd를 통해 마지막 date가 끝난 후 타임라인을 Reload해줄 수 있습니다.
그림으로 살펴보시면 정확히 저희의 코드와 일치합니다. 초기 위젯이 화면에 보여지고 이를 Reload해서 Provide timeLine으로 흘러들어옵니다. 저희의 배열에 초창기 now값은 0을 더한 값인 현재시간이 되겠네요.

배열의 마지막번째에 도달했기 때문에 Provider는 현재 시간에 대한 단일 항목과 로 설정된 새로 고침 정책으로 응답하게 됩니다. 마무리가 되면 새 타임라인을 요청하도록 지시할 때까지 다른 타임라인을 요청하지 않습니다.

새로운 타임라인도 만들 수 있어요!

주식을 예로 들어 볼께요. 주식 개장은 평일 정해진 시간에만 진행하고 주말에는 진행하지 않습니다. 따라서 금요일의 장 시간 마지막에 날짜를 지정해서 주말을 스킵할 수 있습니다. 이러한 결과로 주식시장이 열릴 때까지 위젯을 업데이트할 필요가 없어진 것입니다.

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

기존 .atEnd에서 .after로 바꿔 준다면 해당 Date 날짜가 지난 후 타임라인을 리로딩 하게 수정할 수 있는 것입니다.

SimpleEntry

struct SimpleEntry: TimelineEntry {
	let date: Date
	let configuration: ConfigurationIntent
}

기본적으로 TimelineEntry를 채택해야 합니다.

필수구현 프로퍼티로는 date가 있어요. 위젯에 표시할 날짜를 지정하고 타임라인 항목을 생성하기 위해 필요합니다. configuration은 필수로 구현하지 않아도 됩니다.
위젯 extension을 할 때 Configuration Intent를 체크했다면 자동적으로 따라붙게 됩니다. 해당 프로퍼티는 동적으로 뷰를 구성할 때 필요해요.

WidgetEntryView

 struct MyWidgetEntryView: View {
 	@Environment(\.widgetFamily) var family: WidgetFamily
	var entry: Provider.Entry
        
	var body: some View {
		switch family {
		case .systemSmall:
		SmallView()
		case .systemMedium:
		MediumView()
		case .systemLarge:
		LargeView()
		default:
		Text("none")
		}
	}
}

랜더링 되어서 실제로 보여줄 뷰를 구성하는 구조체입니다.
WidgetFamily라는 것을 통해 다양한 사이즈의 뷰를 구성할 수 있습니다. 여기서 엔트리를 받아 해당 뷰에 주입할 수 있습니다.

Widget

struct MyWidget: Widget {
	let kind: String = "MyWidget"
        
	var body: some WidgetConfiguration {
		IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
			MyWidgetEntryView(entry: entry)
		}
		.configurationDisplayName("My Widget")
		.description("This is an example widget.")
	}
}

마지막으로 Widget입니다. kind에는 위젯의 id를 작성할 수 있습니다. intent에는 어떤것을 동적으로 작동하는 지에 대해 설정할 수 있어요. provider에는 위에서 설명드린 Provider를 주입시키면 되겠고, entry 또한 주입시켜줄 수 있습니다.

configurationDisplayName를 통해 위젯 갤러리라고 불리는 위젯 추가하는 화면에서의 상단에 위치한 타이틀 이름을 설정할 수 있습니다.

description 또한 마찬가지로 해당 크기의 위젯에 대한 설명 또는 전역적인 설명을 추가할 수 있어요.

💡 entry를 작성할 때 configuration을 보셨을텐데 해당 네이밍은 위 파일에서 파생되어 나오게 됩니다.


Configuration은 워낙 다양하기 때문에 이것만으로 포스팅을 따로 진행하겠습니다. 동적으로 무언가를 구성한다로 지금은 이해하시고 넘어가셔도 무방합니다.

이번에 위젯을 추가해서 작업한 프로젝트로 잘 사용할 일이 없다시피했는데 좋은 기회가 되서 위젯을 만들어 볼 수 있었습니다. 🐣

profile
아요쓰 정벅하기🐥

8개의 댓글

comment-user-thumbnail
2024년 2월 28일

프로젝트할때마다 UIkit으로 하는바람에 언젠가는 UIkit프로젝트중에서도 위젯할만한 플로젝트는 스유를 도입해서 위젯을만들어보겠다고 다짐하지만 아직 한번도 시도해본적은 없네요 ㅠ 역시 위젯관련해서도 공식문서가 참 친절하게 작성되어있군요 timer line이 생각보다 중요한 요소로 자리잡고있네요 ㅎㅎ 흥미롭게 읽었습니다:)

1개의 답글
comment-user-thumbnail
2024년 2월 28일

WidgetKit은 SwiftUI로만 되나 보군요
제가 쓰는 앱들도 위젯을 지원하지만 막상 자주 쓰지는 않는 것 같아요
제가 제일 자주 쓰는 건 미리 알림 정도랄까요?
미리 알림 위젯은 항목 터치하면 완료 처리가 되던데 이런건 어떻게 하는 건지 알아보는 것도 좋을 것 같아요
사용자와의 인터랙션을 어떻게 처리하는지랄까요?

1개의 답글
comment-user-thumbnail
2024년 3월 2일

위젯으로 만들어 보고 싶은 것들이 꽤 있었는데 지속적인 리로딩이 제약이 있다면 구현이 꽤 어렵겠네요 ㅜ
글 중간에 주식 관련 예시 덕분에 이해가 더 잘 되는 것 같습니다 👍
혹시나 다음에 위젯을 구현할 일이 생긴다면 다시 보러 오겠습니다 !! 좋은 글 감사합니다🔥

1개의 답글
comment-user-thumbnail
2024년 3월 2일

SwiftUI를 조금씩 파보고 있다보니 흥미롭게 읽어볼 수 있었네요! 감사합니다 :)
단순히 앱을 구현하는데만 조금씩 사용해봤을 뿐, 위젯 관련해서는 찾아보지 않았었는데
참고해서 다음에 활용해봐야겠네요.

그리고 위젯을 잘 사용하지 않는 편이다보니 다소 이상한 궁금증이 생겼는데요! 😅
처음에는 유용할 것 같아 어플을 받고 위젯까지 만들었는데, 위젯은 두고
어플만 홈화면에서 삭제를 할 경우, 위젯 업데이트 주기는 어떻게 바뀌는걸까요...?

1개의 답글