[새싹 iOS] 20주차_WidgetKit

임승섭·2023년 11월 28일
1

새싹 iOS

목록 보기
36/45
post-thumbnail

WidgetKit

  • iOS 14부터 도입
  • SwiftUI로만 구현 가능 (UIViewRepresentable 불가능)
  • 위젯은 미니앱이 아니다
    • 사용자에게 정보를 보여주기 위한 도구에 불과
    • 위젯 자체가 하나의 앱 기능을 할 수는 없다
    • 메모리 30MB 제약
  • 버전
    • iOS 14 : 홈 화면, 오늘 보기
    • iOS 16 : + 잠금 화면
    • iOS 17 : + Mac Desktop, iPad Lock Screen, StandBy, Watch Smart Stack
  • Widget Configuration (속성 편집에 대한 기능)
    • Static Configuration
      : 위젯 편집 항목이 나타나지 않으며, 사용자가 설정을 변경할 수 있는 옵션이 없다
    • Intent Configuration
      : 위젯 편집 기능을 통해 사용자가 여러 Intent값을 수정할 수 있도록 위젯을 구성할 수 있다
      : iOS 17부터 'AppIntentConfigutation' 으로 변경
    • Activity Configuration
      : Live Activity

Widget Extension

  • 새로운 타겟으로 'Widget Extension'을 추가한다

  • 기본적으로 폴더 단위로 나눠지게 되고, 다른 타겟이기 때문에 접근 제어자에 따라 기존 프로젝트에 대한 접근 여부가 결정된다.
  • 또는 인스펙터 영역의 Target Membership에서 사용 범위를 정할 수 있다.

코드

  • 위젯 파일은 크게 4가지의 struct로 구성되어 있다.
    • Provider에서 사용자가 설정한 시간에 맞춰 위젯을 업데이트할 수 있게 한다
    • Entry에서 위젯에 필요한 데이터를 제공한다
    • EntryView는 Entry를 통해 구성하며, UI를 담당하는 역할과 유사하다
    • Widget에서는 static, intent, activity인지에 따라 최종적인 위젯을 구성한다

1. Provider

  • "어떤 시간대에 어떻게 업데이트할지"
  • typealias를 통해 사용할 구조체를 정한다
struct Provider: TimelineProvider {
	typealias Entry = SimpleEntry
  • 사용자는 짧은 시간 내에 위젯을 보아야 하기 때문에, 위젯에 로딩이 길면 좋지 않다.
  • 그래서 미리 위젯 뷰를 그리고 있다가 시간에 맞춰 뷰를 업데이트하고, TimelineEntry 배열을 통해 특정 시간에 위젯을 업데이트 할 수 있도록 한다.
  • 즉, Provider는 위젯의 디스플레이를 업데이트할 시기를 WidgetKit에게 알려주는 역할을 수행한다

1. placeholder

  • 위젯을 최초로 렌더링할 때 사용한다 (스켈레톤 뷰 역할)
  • 데이터를 보여줄 때까지 걸리는 시간동안 보여줄 뷰
  • 사용자가 잠금 화면에서 민감한 정보를 숨기도록 선택한 경우 잠금 해제 전까지 placeholder로 위젯을 숨길 수 있다
  • AOD 화면에서 잠금 전까지 보이지 않도록 구성할 수 있다
    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(
            date: Date(),
            emoji: "😀",
            title: "플레이스 홀더 타이틀",
            price: 1200000
        )
    }

2. getSnapshot

  • 위젯 추가할 때 미리보기 화면 (위젯 갤러리)
  • 위젯을 구성하는 데이터가 네트워크 통신을 통해서 가져오거나, 계산하는 데 몇 초 이상 걸릴 경우를 대비해 mock 데이터를 이용해 빠르게 위젯을 그릴 수 있도록 설정할 때도 이용
    func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        let entry = SimpleEntry(
            date: Date(),
            emoji: "😎",
            title: "미리보기 타이틀",
            price: 16000000
        )
        completion(entry)
    }

3. getTimeLine

  • 위젯 상태 변경 시점 (시간에 대한 핸들링)

  • 뷰를 미리 렌더링하고 올린다. (위젯의 작동 방식 - Meet WidgetKit)

  • Widget 상태가 변경될 미래 시간이 포함된 timelineEntry 배열과 timeline 정책을 포함하고 있는 Timeline을 반환한다

  • .atEndTimelineReloadPolicy 구조체에서 설정되어 있는 타입 프로퍼티로, 타임의 마지막 날짜가 지난 후, WidgetKit이 새로운 타임라인을 요청할 수 있도록 지정하는 정책 에 해당한다.

    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        var entries: [SimpleEntry] = []
    
        let currentDate = Date()
    
        // 타임라인 배열
        for hourOffset in 0 ..< 30 {
            let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
    
            let entry = SimpleEntry(
                date: entryDate,
                emoji: "😇",
                title: "타임라인 타이틀",
                price: 2000000
            )
    
            entries.append(entry)
        }
    
        let timeline = Timeline(entries: entries, policy: .atEnd)
        // .never : 요청 x
        // .after : 특정 시간 이후
    
        completion(timeline)
    }

2. Entry

  • 위젯을 구성하는 데 필요한 데이터를 갖는다
  • TimelineEntry 프로토콜을 채택한다
    • date : 필수로 가져야 하며, 위젯이 다시 그려질 시간에 대한 정보를 갖는다
    • relevance : 스마트 스택을 가진 위젯에서 위젯의 우선순위를 결정한다. (Score가 높은 위젯이 스택의 최상단으로 올라오도록 설정되어 있다)
    struct SimpleEntry: TimelineEntry {    
        let date: Date
        let emoji: String
        let title: String 
        let price: Int
    }

3. EntryView

  • Provider를 통해 Entry를 제공받으면, Entry를 이용해서 위젯의 뷰를 그려준다

  • Entry를 매개변수로 가지는 SwiftUI View이기 때문에 원하는 UI를 자유롭게 구성할 수 있다

    struct MyCoinOrderBookWidgetEntryView : View {
        var entry: Provider.Entry   // 프로퍼티로 Entry에 대한 정보를 넣어준다
    
        var body: some View {
            VStack {
                Text(entry.date, style: .time)
                Text(entry.emoji)
                Text(entry.title)
                Text(entry.price.formatted())
            }
        }
    }

4. Widget

  • 최종적으로 WidgetConfiguration을 구성한다

  • 동일한 크기의 여러 위젯을 만들 수 있는데, kind는 위젯의 고유한 문자열이다.

  • .configurationDisplayName, description 을 통해 위젯 갤러리에서 보일 위젯의 이름과 설명을 설정할 수 있다

  • .supportedFamilies 를 통해 제공할 위젯의 크기를 설정할 수 있다

    // 위젯의 정보
    struct MyCoinOrderBookWidget: Widget {
        let kind: String = "MyCoinOrderBookWidget"  
    
        var body: some WidgetConfiguration {
            StaticConfiguration(kind: kind, provider: Provider()) { entry in
                if #available(iOS 17.0, *) {
                    MyCoinOrderBookWidgetEntryView(entry: entry)
                        .containerBackground(.fill.tertiary, for: .widget)
                } else {
                    MyCoinOrderBookWidgetEntryView(entry: entry)
                        .padding()
                        .background()
                }
            }
            .configurationDisplayName("보유 코인")  
            .description("실시간 시세를 확인하세요")
            .supportedFamilies([.systemSmall, .systemLarge, .systemMedium])
        }
    }

App Group

  • 위젯과 앱은 엄연히 다른 앱이기 때문에, 데이터를 share하고 싶으면 AppGroup을 만들어 주어야 한다.
    • AppGroup을 통해 App과 App Extension 간 데이터를 공유할 수 있다
    • Extension의 Bundle은 Container App 번들에 포함되어 있지만, 두 가지는 각각 Container를 가지고 있기 때문에 둘 사이에는 데이터가 share되지 않는다.
  • "Singing & Capabilities" 에서 App Group을 추가해준다

Share UserDefaults

  • UserDefaults 역시 App과 App Extension에서 저장되는 위치가 다르기 때문에 shared container를 만들어서 처리하는 과정이 필요하다

1. UserDefaults.groupShared

  • 해당 아이디를 가진 그룹 내에 있는 UserDefaults에 저장할 수 있도록 한다
extension UserDefaults {
    static var groupShared: UserDefaults {
        let appGroupID = "group.widgetTest.myCoinOrderBook"
        return UserDefaults(suiteName: appGroupID)!
    }
}

2. Entry View

struct MyCoinOrderBookWidgetEntryView : View {
    var entry: Provider.Entry   // 프로퍼티로 Entry에 대한 정보를 넣어준다

    var body: some View {
        VStack {
            Text(entry.date, style: .time)
            Text(entry.emoji)
            Text(UserDefaults.groupShared.string(forKey: "Market") ?? "기본값" )
            Text(entry.title)
            Text(entry.price.formatted())
        }
    }
}

3. Setting

.onAppear {
  UserDefaults.groupShared.set(viewModel.market.koreanName, forKey: "Market")
}

WidgetCenter

1. getCurrentConfiguration

  • 현재 활성화되어 있는 위젯 정보를 확인할 수 있다
    WidgetCenter.shared.getCurrentConfigurations { response in
        switch response {
        case .success(let info):
            print(info)
        case .failure(let error):
            print(error)
        }
    }

2. reloadTimeLines

  • 필요한 시점에 위젯을 업데이트할 수 있다

    .onAppear {
        viewModel.fetchOrderBook()
    
        print("----- 현재 활성화 되어 있는 위젯 -----")
        WidgetCenter.shared.getCurrentConfigurations { response in
            switch response {
            case .success(let info):
                print(info)
            case .failure(let error):
                print(error)
            }
        }
    
        print("이전 : ", UserDefaults.groupShared.string(forKey: "Market"))
        UserDefaults.groupShared.set(viewModel.marketData.koreanName, forKey: "Market")
        print("이후 : ", UserDefaults.groupShared.string(forKey: "Market"))
    
        WidgetCenter.shared.reloadTimelines(ofKind: "MyCoinOrderBookWidget")
    }

0개의 댓글