반응형
LinkedIn 개발자로 성장하면서 남긴 발자취들을 확인하실 수 있습니다.
Github WWDC Student Challenge 및 Cherish, Tiramisul 등 개발한 앱들의 코드를 확인하실 수 있습니다.
개인 앱 : Cherish 내 마음을 들여다보는 시간, 체리시는 디자이너와 PM과 함께 진행 중인 1인 개발 프로젝트입니다.
10년 후, 20년 후 나는 어떤 스토리 텔러가 되어 있을지 궁금하다. 내가 만약에 아직 조금 더 탐구하고 싶은 게 있고, 궁금한 게 있다면, 그게 설사 지금 당장의 내 인생에 도움이 안 되는 것 같더라도 경험해보자. 그 경험들을 온전히 즐기며 내 것으로 만들고, 내 일에 녹여내고... 그러다보면 그 점들이 모여 나란 사람을 그려내는 선이 될 테니까.

Recent Posts
Recent Comments
Total
관리 메뉴

꿈꾸는리버리

SwiftUI Widget 딱대.. (1/3) 본문

오뚝이 개발자/SwiftUI

SwiftUI Widget 딱대.. (1/3)

rriver2 2022. 8. 6. 09:27
반응형

 

위젯.... 너무 하고 싶어서 너무 하고 싶어서 너무 해버렸다...

매번 위젯이 너무 하고 싶었었고, 그래서 사이드에서 내가 위젯을 하고 싶다고 자청해서 테스크를 가져가게 되었다 ㅎㅎ

이제 위젯 ~~ 무섭지 않아~~~(?)

 


일단 위젯 공부 공유에 들어가기에 앞서서 요즘 빠진? 공부 방법을 소개하고자 한다.

일명.. "해야 하는 거 먼저 적어보기"

처음 하는 거를 도전하는 일이 빈번해지니까 처음 공부할 때 스스로 잘 공부하는 방법이 없을까..?에 대한 고민을 많이 했었다..

그리고 이렇게 해야 하는 것들을 적어보는게 나에게는 되게 도움이 많이 된다는 것을 알았다.

 

우선 위젯을 하는 이유가 명확했기 때문에 내가 위젯을 통해 구현해야 하는 것들에 대해 나열을 해봤다.

 

해야 하는 건 다음 두 기능이었다. 

1) 위젯에서는 최근 재생한 음악이 보이기

2) 위젯을 탭할 경우 해당 CD의 화면으로 이동하기

 

 

그리고 이 2가지를 하기 위해서 나는 ... 3가지 일을 할 줄 알아야 했다.

1) 앱 내에 있는 데이터를 위젯에 띄우기

2) 위젯에 있는 콘텐츠에 따라 앱 내의 다른 화면으로 열리게 하게

3) 데이터를 저장해두고, 앱내에서 유저가 Play 버튼을 누를 때마다 해당 데이터가 변경되게 하기

 

이렇게 목표를 명확하게 하고, 다음 스텝에 대해 확인해 봄으로 장시간(14시간^^)동안 열받아 하면서 해당 테스크를 해치울 수 있었다 !!

 


자, 그럼 위젯 파헤치기 시작 !!

처음에는 공문을 열심히 읽었다.. 

 

하지만 언제나 시작은 ! 직접 코드를 짜기 시작하기 부터지...

 


애플은 extention한번에 여러 다양한 위젯을 만들 수 있지만, 위젯마다 사용자에게 요구하는 사항(ex 위치)이 다르다면 여러개의 extention을 사용해서 위젯을 구현하라고 했다.

확인해보니 여러 위젯을 extention으로 만들 수 있었고,  같은 정보를 필요로 하는 다른 크기의 위젯을 한 extention에서 처리하는 게 맞다는 생각이 들었다 (맨 뒤에 내용 있음)

 

1️⃣  위젯 만들기 !

다음과 같이 Taget 텝을 통해 widgetextention을 추가하고

 

요로케 추가를 하고나면, widget folder가 생긴 것을 확인할 수 있고

시뮬레이션을 돌리면 위젯을 바로 추가할 수 있다 ! (이 위젯에서는 현재 시간을 확인할 수 있음 !)

 

 

2️⃣ 위젯 코드 파헤치기 

그렇다면.. 친절한 애플이 예시 코드를 작성해 놨다는 거니까 ~~ 신나게 보러 가보자 !!

 

widget프로젝트명.swift 파일을 열면 이렇게 크게 5등분된 녀석들을 확인할 수 있다.

// Provider: 시간에 따른 위젯 업데이트 로직
struct Provider: IntentTimelineProvider {
	// code
}

// TimelineEntry: 위젯을 표시할 날짜를 지정하고 선택적으로 위젯 데이터를 지정함
struct SimpleEntry: TimelineEntry {
	// code
}

// EntryView: 위젯을 표시하는 View
struct widgetEntryView : View {
	// code
}

// widget의 entry point (위젯을 시작하는 부분) 
@main
struct widget: Widget {	
	// code
}

// widget 미리보기
struct widget_Previews: PreviewProvider {
	// code
}

흠흠 보면..

위젯을 보여주기에 필요한 내용들이 쏙쏙 들어가 있다.

 

날씨 위젯 같은 거 보면 앱을 들어가지 않아도 날씨 정보가 계속 바뀌니까 이거는 위젯을 앱을 열지 않아도 업데이트를 하는 거겠지..?

-> Provider랑 연관

 

일정 관리 위젯의 경우에는 내가 앱에서 작성해둔 데이터가 보이는 거는 위젯과 앱이 데이터가 공유 된다는 거고 ... 그럼 데이터가 담겨져 있어야 겠네 ?

-> TimelineEntry와 연관

 

실제 위젯을 꾸미는 View가 있어야 할 테고, small, medium, large, extraLarge 크기를 분기처리 하는 부분도 있어야 겠네 ?

-> EntryView와 연관

 

그리고 위젯도 앱처럼 진입 포인트를 짚어줘야 할 테니...

-> @main, Widget 과 연관

 

 

이제 하나하나 차례대로 봐보자 ~

1) 일단 쉬운 EntryView 부터 ㅎㅎ

// EntryView: 위젯을 표시하는 View
struct widgetEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        Text(entry.date, style: .time)
    }
}

코드를 보면... 그냥 쉽다. entry는 데이터를 저장하는 거라고 했으니까, entry의 date 정보를 time의 스타일로 보여준다.

 

+) style 지정을 처음봐서 신기한 마음에 .timer로 바꿔보니까.. 이렇게 타이머가 되더라..? ㅋㅋㅋㅋㅋㅋ 몰랐습니다..

 

2) 다음에는 TimelineEntry !

이 친구는 뭔가 느낌이 앱에서의 model 과 비슷하다는 냄새가 솔솔 났다.

// TimelineEntry: 위젯을 표시할 날짜를 지정하고 선택적으로 위젯 데이터를 지정함
struct SimpleEntry: TimelineEntry {
    let date: Date
    let configuration: ConfigurationIntent
}

 

TimelinEntry는 프로토콜로 var data: Date property 를 필수적으로 요구한다.

( 앞에서 위젯이 업데이트 되어야 한다고 했잖슴 ? 그래서 언제 갱신이 되었는지를 확인하기 위해서 date를 요구하는 거 같았다..!

+) relevance는 스마트 위젯이랑 관련된 거라고 하는데 지금은 안 쓸 거 같아서 skip 했다 )

 

 

그러면 date만 넣을 수 있냐 !! 그건 당연히 아니지 !!

struct CDWidgetEntry: TimelineEntry {
    let date: Date  // 필수
    let imageName: String
    var id: Int
    var name: String
    var url: URL
}

나는 다른 property들도 필요해서 다음과 같이 imageName, id 등을 추가해서 사용했다.

앞서 말한 것처럼 TimelineEntry은 위젯을 표시할 날짜를 지정하고 이처럼 "선택적"으로 위젯 데이터를 지정한다..! 

 

+) TimelineEntry에 다른 property가 있으면 당연히 앞에서 알아본 View에서 해당 property를 사용할 수 있겠죵 ?

// ex) 
struct RelaxOnWidgetEntryView : View {
    var entry: Provider.Entry
    
    var body: some View {
        VStack {
            Image(entry.imageName)
                .resizable()
                .scaledToFit()
            Text(entry.name)
        }
        .padding(.vertical, 20)
    }
}

 

3) 이번에는 살짝 매운 맛 IntentTimelineProvider

// Provider: 시간에 따른 위젯 업데이트 로직
struct Provider: IntentTimelineProvider {
    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: Date(), configuration: ConfigurationIntent())
    }
// 위젯 갤러리에서 샘플로 보여질 부분
    func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        let entry = SimpleEntry(date: Date(), configuration: configuration)
        completion(entry)
    }
// 정의한 타임라인에 맞게 업데이트해서 보여질 내용
    func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        var entries: [SimpleEntry] = []

        // Generate a timeline consisting of five entries an hour apart, starting from the current 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, configuration: configuration)
            entries.append(entry)
        }

        let timeline = Timeline(entries: entries, policy: .atEnd)
        //  policy: 이 타입이 위젯에 새로운 타임라인을 제공해주는 시기를 지정할 수 있도록 해줌
        //  .atEnd - 현재주어진 타임라인이 마지막일 때 새로 타임라인을 요청
        //  .after - 해당 date후에 새로운 타임라인 요청
        //  .never - 필요할 때에 새로운 타임라인을 요청
        completion(timeline)
    }
}

 

정의를 보면 IntentTimelineProvider 이 녀석도 protocol이다. 

 

일단 우선적으로 알아야 하는게 , widget의 configuration에는 2가지가 있다.

첫번째는 StaticCongiguration이고, 두번째는 IntentConfiguration이다.

이름에서 알 수 있는 것처럼 StaticCongiguration가 뭔가 기본인 거 같고 IntentConfiguration는 추가적인 기능을 하는 것을 알 수 있다.

애플에서 말하는 바에 따르면

StaticCongiguration는 사용자 구성 가능한 속성이 없는 위젯의 경우. 예를 들어, 일반적인 시장 정보를 표시하는 주식 시장 위젯이나 트렌드 헤드라인을 표시하는 뉴스 위젯이 있습니다.
IntentConfiguration는 사용자 구성 가능한 속성이 있는 위젯의 경우. SiriKit 사용자 정의 의도를 사용하여 속성을 정의합니다. 예를 들어 도시의 우편번호가 필요한 날씨 위젯이나 추적 번호가 필요한 패키지 추적 위젯이 있습니다.

이라고 한다.

StaticConfiguration은 사용자가 따로 설정을 해서 기호에 맞게 변경하지 않는 위젯이고,

IntentConfiguration은 날씨 앱처럼 어느 지역을 설정할 것인지 등의 사용자가 따로 설정을 할 수 있는 위젯이다. 

애플에서 제공해주었던 기본 앱은 IntentTimelineProvider을 사용하는 것으로 보아 IntentConfiguration 인 것 같았다. 

근데 나는 StaticConfiguration이 필요하니까 ㅎㅎ IntentConfiguration은 깊게 공부하지 않았다.. (일단 해야 하는 것부터 하고, 나머지를 공부하는 게 에너지적으로 소비가 적어서...)

 

그래서 시작하는 StaticConfiguration의 TimelineProvider 설명 ㅎㅎ

placeholder 이 친구는 아직 뭐하는 녀석인지 잘 모르겠다.. (아는 분 있으면 알려줘요효...)

 

getSnapshot 함수의 리턴 값이 entry인 것을 확인할 수 있다.

이 함수는 아래 사진처럼 widget gallery에서 보여주는 위젯의 entry를 정해주는 함수이다. 

// 위젯 갤러리에서 샘플로 보여질 부분
    func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        let entry = SimpleEntry(date: Date(), configuration: configuration)
        completion(entry)
    }

 

 

getTimeline은 정의한 타임라인에 맞게 entry를 업데이트하는 함수이다. 이전에 언급한 것처럼 위젯이 가끔 리로드를 하는데 그때마다 호출되는 함수이다. 

// 정의한 타임라인에 맞게 업데이트해서 보여질 내용
    func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        var entries: [SimpleEntry] = []

        // Generate a timeline consisting of five entries an hour apart, starting from the current 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, configuration: configuration)
            entries.append(entry)
        }

        let timeline = Timeline(entries: entries, policy: .atEnd)
        //  policy: 이 타입이 위젯에 새로운 타임라인을 제공해주는 시기를 지정할 수 있도록 해줌
        //  .atEnd - 현재주어진 타임라인이 마지막일 때 새로 타임라인을 요청
        //  .after - 해당 date후에 새로운 타임라인 요청
        //  .never - 필요할 때에 새로운 타임라인을 요청
        completion(timeline)
    }
}

 

반환 값을 보면 Timeline<EntryType>인 것을 확인 할 수 있다. Timeline<EntryType> 이거는 struct인데, entriy 배열과 policy property를 가지고 있다.

 

policy? 이건 뭐지 ? 한국어로는 정책이라는데.... ㅋㅋㅋ

policy는 TimelineReloadPolicy의 타입이다.

또 찾아보면 이것도 struct인데, 3가지 종류가 있었다.

atEnd, never, after

let currentDate = Date()
for hourOffset in 0 ..< 5 {
    let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
    let entry = SimpleEntry(date: entryDate, configuration: configuration)
   	entries.append(entry)
}

위 코드에서 보면, 현재가 1시 일때, [1시, 2시, 3시, 4시, 5시] 이렇게 값이 들어가게 된다.

 

그러면 5시 이후에는 렌더링을 안 하는 건가 ..?

-> .atEnd을 통해 현재 주어진 타임라인이 마지막일 때 다시 새로 타임라인을 요청한다. 뭐 이렇게 반복이 되는 거라고 한다 ! (실험은 안해봄)

 

다음 .after은 어떤 시간이 지난 후에 reload 되게끔 한다. (실험은 안해봄)

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

 

마지막으로는 .never !! 내가 쓸 녀석이지 ㅎㅎ 

앱 내에서 사용자가 어떤 값을 변경할 때에만 트리거로 해당 위젯을 reload 되게 하는 것이다.

트리거가 필요할 때 해당 코드를 넣으면 된다.

// import WidgetKit 해야함 !

WidgetCenter.shared.reloadTimelines(ofKind: "RelaxOnWidget") // 이거는 하나의 extention widget만 렌더링할 때
// or
WidgetCenter.shared.reloadAllTimelines() // 이거는 모든 위젯을 렌더링 할 때

예를 들면 이런식으로 !

그러면 버튼을 누를 때마다 위젯이 업데이트 되는 것을 확인할 수 있다 !

 

4) 드뎌 마지막 @main Widget 이다 !

// widget의 entry point (위젯을 시작하는 부분) 

@main
struct widget: Widget {
    let kind: String = "widget"
 	// WidgetConfiguration: 위젯 식별 및 위젯의 Content표시
    var body: some WidgetConfiguration {
        //    kind: 위젯의 identifier
        //    provider: 렌더링할 시기를 WidgetKit에 알려주는 타임라인을 생성함
        //    클로져: SwiftUI 뷰를 포함. WidgetKit는 이를 호출하여 내용을 렌더링하고 provider로부터 타임라인 entry 파라미터를 전달함
        IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
            widgetEntryView(entry: entry)
        }
        .configurationDisplayName("My Widget")  // 위젯 설명 타이틀
        .description("This is an example widget.")  // 위젯 설명 부타이틀
        .supportedFamilies([.systemSmall]) // 위젯 크기 설정
    }
}

우선 WidgetConfiguration은 해당하는 위젯을 불러오고, 위젯의 Content를 표시하는 역할을 한다. 

애플의 예시에는 앞서 설명했던 IntentConfiguration을 만들었고, kind에는 위젯의 identifier(위젯명인 거 같음)와 intent(IntentTimelineProvider에서 요구했던 property) 그리고 provider, 마지막으로는 클로져를 작성한다. 클로져에서는 앞서 넣은 provider로 부터 받은 entry를 받는다 ! 

 

 

 

+) WidgetFamily

WidgetFamily을 이용하면 같은 data를 쓰지만, 다른 모양의 위젯을 만들 수 있다.

struct widgetEntryView : View {
    @Environment(\.widgetFamily) var family: WidgetFamily
    var entry: Provider.Entry
    
    var body: some View {
        switch family {
            case .systemSmall:
                Text(entry.date, style: .time)
            case .systemMedium:
                Text("systemMedium")
            case .systemLarge:
                Text("systemLarge")
            case .systemExtraLarge:
                Text("systemExtraLarge")
            default:
                EmptyView()
        }
    }
}

 

 

약간 ... 호다닥 끝내버린 감이 없지 않아 있는데.. 조만간 한번 더 정리를 해야겠다... 

반응형
Comments