WidgetKit (2)

 

Dynamic Month 적용

CleanShot 2024-12-03 at 20 51 15

Config

먼저 파일을 만드는데 일반 Swift File로 만든다.

이때 중요한점

CleanShot 2024-12-03 at 20 48 29

target을 어떤것에 적용할지 반드시 확인하자.

struct MonthConfig {
    let backgroundColor: Color
    let emojiText: String
    let weekdayTextColor: Color
    let dayTextColor: Color
    
    static func determineConfig(from date: Date) -> MonthConfig {
        let monthInt = Calendar.current.component(.month, from: date)
        
        switch monthInt {
        case 1:
            return MonthConfig(backgroundColor: .gray,
                               emojiText: "⛄️",
                               weekdayTextColor: .black.opacity(0.6),
                               dayTextColor: .white.opacity(0.8))
        case 2:
            return MonthConfig(backgroundColor: .palePink,
                               emojiText: "❤️",
                               weekdayTextColor: .black.opacity(0.5),
                               dayTextColor: .pink.opacity(0.8))
        //... 후략...
        }
    }
}

View에 적용

struct MonthlyWidgetEntryView : View {
    var entry: DayEntry
    var config: MonthConfig
    
    init(entry: DayEntry) {
        self.entry = entry
        self.config = MonthConfig.determineConfig(from: entry.date)
    }

    var body: some View {
        ZStack {
            VStack {
                HStack {
                    Text(config.emojiText)
                        .font(.title)
                    Text(entry.date.weekDayDisplayFormat)
                        .font(.title3)
                        .fontWeight(.bold)
                        .minimumScaleFactor(0.6)
                        .foregroundStyle(config.weekdayTextColor)
                    Spacer()
                }
                Text(entry.date.dayDisplayFormat)
                    .font(.system(size: 80, weight: .heavy))
                    .foregroundStyle(config.dayTextColor)
            }
            .padding(2)
        }
        .containerBackground(config.backgroundColor.gradient, for: .widget)
    }
}

init을 해주되, 설정값같은 config는 init할때 monthConfig에서 가져오게 했다.

preview에 적용

이전엔 preview역시도 struct로 존재했으나, 지금은 그렇지 않기에

만들어준다.

struct MonthlyWidgetEntryView_Previews: PreviewProvider {
    static var previews: some View {
        MonthlyWidgetEntryView(entry: DayEntry(date: dateToDisplay(month: 3, day: 22), configuration: .smiley))
            .previewContext(WidgetPreviewContext(family: .systemSmall))
    }
    
    static func dateToDisplay(month: Int, day: Int) -> Date {
        let components = DateComponents(calendar: Calendar.current,
                                        year: 2024,
                                        month: month,
                                        day: day)
        return Calendar.current.date(from: components)!
    }
}

이러면 자동으로 preview 적용이 된다.

iOS17 적용

강의는 이전에 만들어졌기에 이전글에서 containerBackground에 대한 언급이 없었다.

이부분이 새롭게 추가된 내용이라 코드를 첨부한다.

containerBackground 적용

var body: some View {
    ZStack {
        VStack {
            HStack {
                Text(config.emojiText)
                    .font(.title)
                Text(entry.date.weekDayDisplayFormat)
                    .font(.title3)
                    .fontWeight(.bold)
                    .minimumScaleFactor(0.6)
                    .foregroundStyle(config.weekdayTextColor)
                Spacer()
            }
            Text(entry.date.dayDisplayFormat)
                .font(.system(size: 80, weight: .heavy))
                .foregroundStyle(config.dayTextColor)
        }
        .padding(2)
    }
    .containerBackground(for: .widget){
        ContainerRelativeShape()
            .fill(config.backgroundColor.gradient)
    }
}

이렇게 containerBackground에 담아주었다.

실행화면은 같다.

Standby 적용

그리고 새롭게 standby mode가 나오면서 잠금을 해두었을때도 나타나는데,

CleanShot 2024-12-04 at 13 35 05

이렇게 11월일때는 검은색이라서 안보이게 된다.

이걸 방지하기위해 환경변수를 적용한다.

struct MonthlyWidgetEntryView : View {
    @Environment(\.showsWidgetContainerBackground) var showsBackground
    
    var entry: DayEntry
    var config: MonthConfig
    
    init(entry: DayEntry) {
        self.entry = entry
        self.config = MonthConfig.determineConfig(from: entry.date)
    }
    
    var body: some View {
        ZStack {
            VStack {
                HStack {
                    Text(config.emojiText)
                        .font(.title)
                    Text(entry.date.weekDayDisplayFormat)
                        .font(.title3)
                        .fontWeight(.bold)
                        .minimumScaleFactor(0.6)
                        .foregroundStyle(showsBackground ? config.weekdayTextColor : .white)
                    Spacer()
                }
                Text(entry.date.dayDisplayFormat)
                    .font(.system(size: 80, weight: .heavy))
                    .foregroundStyle(showsBackground ? config.dayTextColor : .white)
            }
            .padding(2)
        }
        .containerBackground(for: .widget){
            ContainerRelativeShape()
                .fill(config.backgroundColor.gradient)
        }
    }
}

showsWidgetContainerBackground의 동작 방식

  1. true일 때:
    • 위젯이 홈 화면이나 잠금 화면 등에서 컨테이너 배경과 함께 표시되는 경우.
    • 일반적으로 위젯의 배경이 시스템에 의해 제공되는 영역에 포함될 때.
  2. false일 때:
    • 위젯이 대기 모드(Standby Mode)나 특정 상황에서 컨테이너 배경 없이 표시되는 경우.
    • 이 경우 위젯은 투명한 배경 위에 표시되므로, 명시적으로 배경을 추가해줘야 할 수 있다.

또한 Night Mode에서는 다르게 하고싶다면

@Environment(\.widgetRenderingMode) var renderingMode 이걸 추가해준다.

그리고 LockScreen이나, standby등 어떤 조건에서는 위젯을 사용하고 싶지 않다면

var body: some WidgetConfiguration {
    AppIntentConfiguration(kind: kind, intent: ConfigurationAppIntent.self, provider: Provider()) { entry in
        MonthlyWidgetEntryView(entry: entry)
            //.containerBackground(.gray.gradient, for: .widget)
    }
    .configurationDisplayName("Monthly Style Widget")
    .description("The theme of the widget changes based on month.")
    .supportedFamilies([.systemSmall])
    .disfavoredLocations([.homeScreen], for: [.systemSmall])
}

이런식으로 disfavored locations을통해 설정해주면 된다.

그리고 위의 preview역시 이제는 그렇게 지원하지 않기에,

struct MockData {
    static let dayOne = DayEntry(date: dateToDisplay(month: 9, day: 4), configuration: ConfigurationAppIntent())
    static let dayTwo = DayEntry(date: dateToDisplay(month: 10, day: 5), configuration: ConfigurationAppIntent())
    static let dayThree = DayEntry(date: dateToDisplay(month: 11, day: 6), configuration: ConfigurationAppIntent())
    static let dayFour = DayEntry(date: dateToDisplay(month: 12, day: 7), configuration: ConfigurationAppIntent())
    
    
    static func dateToDisplay(month: Int, day: Int) -> Date {
        let components = DateComponents(calendar: Calendar.current,
                                        year: 2022,
                                        month: month,
                                        day: day)
        
        return Calendar.current.date(from: components)!
    }
}

configuration은

CleanShot 2024-12-04 at 15 13 55 여기서 체크를 풀었는데 이걸 체크하면 생기는것이다. (12.04 추가)

해당 프로젝트를 만들때는 아무생각없이 체크를해서 생겨났다.

그러면

struct RepoWatcherWidget: Widget {
    let kind: String = "RepoWatcherWidget"

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            if #available(iOS 17.0, *) {
                RepoWatcherWidgetEntryView(entry: entry)
                    .containerBackground(.fill.tertiary, for: .widget)
            } else {
                RepoWatcherWidgetEntryView(entry: entry)
                    .padding()
                    .background()
            }
        }
        .configurationDisplayName("My Widget")
        .description("This is an example widget.")
    }
}

이런식으로 초기에 코드가 작성이된다.

AppIntentConfiguration(kind: kind, intent: ConfigurationAppIntent.self, provider: Provider())

여기에 intent가 없었다.

그리고 애초에 다른게

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

Configuration앞에가 다르다. (여기까지가 12.04 수정)

그러면 여러 preview들을 볼수있다.

CleanShot 2024-12-04 at 13 50 10

애니메이션 추가

그리고 숫자의 바뀜을 좀더 역동적으로 하기위해

.contentTransition(.numericText())이걸 추가

Text(entry.date.dayDisplayFormat)
    .font(.system(size: 80, weight: .heavy))
    .foregroundStyle(showsBackground ? config.dayTextColor : .white)
    .contentTransition(.numericText())

Dec-04-2024 13-51-13

HStack {
    Text(config.emojiText)
        .font(.title)
    Text(entry.date.weekDayDisplayFormat)
        .font(.title3)
        .fontWeight(.bold)
        .minimumScaleFactor(0.6)
        .foregroundStyle(showsBackground ? config.weekdayTextColor : .white)
    Spacer()
}
.id(entry.date)
.transition(.push(from: .trailing))
.animation(.bouncy, value: entry.date)

요일쪽도 해보면.

Dec-04-2024 13-55-30

이렆게 된다.

iOS18 적용

새롭게 추가된 기능중 tinted가 있는데

Dec-04-2024 14-00-38

현재는 위젯만 적용이 안되고 있다.

이부분을 해결해보자.

아주 간단하다.

.widgetAccentable()이걸 추가해주면 된다.

Text(entry.date.dayDisplayFormat)
    .font(.system(size: 80, weight: .heavy))
    .foregroundStyle(showsBackground ? config.dayTextColor : .white)
    .contentTransition(.numericText())
    .widgetAccentable()

Dec-04-2024 14-00-38

이젠 잘되는걸 알수있다.

하지만 하나 문제라면 지금 위에 트리의 색이 사라지고 하얗게 되버린다.

Forum에 관련 이슈를 언급하는 내용이 있어 해결해본다.

   .background(Color.black)
   .compositingGroup()
   .luminanceToAlpha()

이걸 사용해서 해결이 된다고하니 적용해본다.

Text(config.emojiText)
    .font(.title)
    .background(Color.black)
    .compositingGroup()
    .luminanceToAlpha()

simulator_screenshot_4A0B8824-2BB1-465F-9D8E-79EF5460C5AE

이렇게 나오는걸 알 수 있다.

요일과 emoji 모두 색상을 tint에 적용하려면

Hstack에 .widgetAccentable() 만 적용해주면 끝.

그부분은 생략한다.