포스트

WidgetKit (6)

App intents?

iOS 17에서 애플은 APP Intents를 소개한다.

그렇다면 App Intents는 무엇일까?

이건 WWDC2024

Docs에서는 다음과 같의 정의를 한다.

The App Intents framework provides functionality to deeply integrate your app’s actions and content with system experiences across platforms, including Siri, Spotlight, widgets, controls and more.

App Intents 프레임워크는 Siri, Spotlight, 위젯, 컨트롤 등을 포함한 플랫폼 전반의 시스템 경험과 앱의 동작 및 콘텐츠를 심층적으로 통합하는 기능을 제공합니다.

  • App Intents는 앱이 사용자와 상호작용하기 위해 제공하는 인터페이스를 정의하는 새로운 프레임워크.
  • Siri, Spotlight, Shortcuts, 그리고 위젯과 같은 시스템 전반에서 동작.
  • 앱의 기능을 더 쉽게 노출하고, 사용자 경험을 향상.

기존에 했던 Monthly 위젯을 가지고 App Intents를 적용 해보려고한다.

intent 파일 생성

CleanShot 2024-12-06 at 00 20 00

widget을 만든게 아닌 일반적으로 우리가 파일을 만들때처럼 파일을 생성하고 intent로 검색하면 바로 나온다.

CleanShot 2024-12-06 at 00 21 55

만들면 그냥 빈화면이 덩그러니 나온다.

CoreData에서 Entity 추가하듯 아래의 +를 눌러서 새로운 Intent를 그냥 만들어 주면 된다.

CleanShot 2024-12-06 at 00 23 58

그리고 다음과 같이 해준다.

이때 4개의 Check Box가 있는데 우리는 시리는 쓰지않고 Widget에만 할것이므로 2번쨰것만 체크를 해주었다.

CheckBox 의 내용.

  1. User confirmation required:
    • Intent를 실행하기 전에 사용자의 확인이 필요한 경우 체크.
    • 예: 중요한 작업 (삭제, 결제 등)에서 사용자 동의 요청.
  2. Intent is eligible for widgets:
    • 이 Intent가 위젯에서 사용 가능하도록 설정.
    • 체크하면 위젯 UI에서 Intent를 호출할 수 있음.
    • 예: 위젯 버튼을 누르면 “Change Font” Intent 실행.
  3. Intent is user-configurable in the Shortcuts app and Add to Siri:
    • 사용자가 Shortcuts 앱에서 이 Intent를 추가/구성 가능하도록 설정.
    • 예: 사용자가 Siri 명령어로 이 Intent를 호출하도록 추가 가능.
  4. Intent is eligible for Siri Suggestions:
    • Intent가 Siri 제안에 나타나도록 설정.
    • 예: 사용자가 자주 사용하는 경우 Siri가 자동으로 추천.

Category는 관련 정보를 더 찾게 되면 추후 업뎃을 하는걸로…

intent Parameter 설정

CleanShot 2024-12-06 at 00 37 18

이것도 아래에 있는 +를 클릭하여 다음과 같이 만들어준다.

이때 Type이 Boolean인 이유는 해당 기능을 토글(true/false)일때마다 폰트를 적용했다, 풀었다 할것이기때문!

그리고 여기서 Parent Parameter가 나오는데,

이것도 이후에 좀 더 정보를 찾게되면 서술하도록 하겠다.

CleanShot 2024-12-06 at 00 49 05

이렇게 만들고 확인해보면

이렇게 클래스로 파일이 만들어지는것을 알 수 있다.

Widget에서 Intent 적용

CleanShot 2024-12-06 at 00 53 10

이전에도 한번 언급을 했지만, 위젯을 만들때 해당 내용을 체크하냐 안하냐의 따라 Widget이 다르게 설정이 되는데

  1. 체크를 한 경우
    1
    2
    3
    4
    5
    
    var body: some WidgetConfiguration {
         AppIntentConfiguration(kind: kind, intent: ConfigurationAppIntent.self, provider: Provider()) { entry in
             MonthlyWidgetEntryView(entry: entry)
             //.containerBackground(.gray.gradient, for: .widget)
         }
    
    • 이렇게 AppIntentConfiguration으로 되어있다.
  2. 체크를 안한 경우
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    var body: some WidgetConfiguration {
         StaticConfiguration(kind: kind, provider: CompactRepoProvider()) { entry in
             CompactRepoEntryView(entry: entry)
                 .containerBackground(.fill.tertiary, for: .widget)
         }
         .configurationDisplayName("Repo Watcher")
         .description("Keep an eye on one or two Github.")
         .supportedFamilies([.systemMedium, .systemLarge])
     }
    
    • 이렇게 StaticConfiguration으로 되어있다.

즉 StaticConfiguration의 경우는 말그대로 Configuration이 고정 되어있다는것이다.

우리가 Customizing을 할 수 없다는것.

그리고 이때 만약 체크를 하지않고 위젯을 만들었다면

Provider도 전부 다르게 되어있다.

TimelineProvider / IntentTimelineProvider

그리고 AppIntentTimelineProviderIntentTimelineProvider를 적용했을때 전자가 에러가 발생하여 강의와 똑같이 후자로 하였다.

AppIntentConfigurationIntentConfiguration 경우도 마찬가지.

Docs에서 기본 정의만 놓고보면 거의 같은 역할을 한다.

버전차이만 존재

Entry에 FunFont 추가

1
2
3
4
struct DayEntry: TimelineEntry {
    let date: Date
    let showFunFont: Bool
}

이렇게 추가하면 늘 발생하는 Missing Error.

이부분에 대한 내용은 생략.

Timeline 함수에 추가

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func getTimeline(for configuration: ChangeFontIntent, in context: Context, completion: @escaping @Sendable (Timeline<DayEntry>) -> Void) {
        var entries: [DayEntry] = []
        
        let showFunFont = configuration.funFont == 1
        
        // Generate a timeline consisting of seven entries an hour apart, starting from the current date.
        let currentDate = Date()
        for dayOffset in 0 ..< 7 {
            let entryDate = Calendar.current.date(byAdding: .day, value: dayOffset, to: currentDate)!
            let startOfDate = Calendar.current.startOfDay(for: entryDate)
            let entry = DayEntry(date: startOfDate, showFunFont: showFunFont)
            entries.append(entry)
        }
        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }

이렇게 추가를 하는데

CleanShot 2024-12-06 at 05 18 17

NsNumber는 0,1 로 이루어져 있고

0 = false 1 = true 를 의미한다.

View에 추가

Fonts에 가보면 업데이트는 끊겼지만, 대부분 사용가능한 폰트들에 대해 나와있다.

1
2
3
let funFontName = "Chalkduster"

.font(entry.showFunFont ? .custom(funFontName, size: 24) : .title3)

이런식으로 적용하고자하는 부분에 삼항연산자를 통해 적용을 할지 말지를 해주면 된다.

CleanShot 2024-12-06 at 05 25 32CleanShot 2024-12-06 at 05 25 38

이렇게 true / false에 따라 폰트가 다르게 적용되는걸 볼 수 있다.

Dec-06-2024 06-10-17

iOS17 적용

위에서 언급했든 AppIntent가 소개 되었고, 그부분을 적용해본다.

CleanShot 2024-12-06 at 05 34 14

우리가 이전에 만든 Custom Intents를 클릭해서 해주면 된다.

Dec-06-2024 05-35-34

우선 Appintent를 import 해주자.

새로운 구조체 생성

위의 방법대로 Convert를 할 수 있지만, 여기선 기존 Custom Intents를 지우고 새롭게 만드는 방식을 사용했다.

1
2
3
4
5
6
7
struct ChangeFontIntent: AppIntent, WidgetConfigurationIntent {
    static var title: LocalizedStringResource = "Fun Font"
    static var description: IntentDescription = .init(stringLiteral: "Switch to a fun font")
    
    @Parameter(title: "Fun Font")
    var funFont: Bool
}

그리고 기존의 IntentTimelineProvider 의 내부 함수인 getSnapshot, getTimeline의 경우 Comepletion Handler가 있었는데 AppIntentTimelineProvider의 내부함수에서는 그게 사라지고 Return Type이 생기고 리턴을 하도록 바뀌었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// before
struct Provider: IntentTimelineProvider {
    func getSnapshot(for configuration: ChangeFontIntent, in context: Context, completion: @escaping (DayEntry) -> Void) {
        let entry = DayEntry(date: Date(), showFunFont: false)
        completion(entry)
    }

    func getTimeline(for configuration: ChangeFontIntent, in context: Context, completion: @escaping (Timeline<DayEntry>) -> Void) {
    var entries: [DayEntry] = []

    let showFunFont = configuration.funFont == 1

    // Generate a timeline consisting of seven entries a day apart, starting from the current date.
    let currentDate = Date()
    for dayOffset in 0 ..< 7 {
        let entryDate = Calendar.current.date(byAdding: .day, value: dayOffset, to: currentDate)!
        let startOfDate = Calendar.current.startOfDay(for: entryDate)
        let entry = DayEntry(date: startOfDate, showFunFont: showFunFont)
        entries.append(entry)
    }

    let timeline = Timeline(entries: entries, policy: .atEnd)
    completion(timeline)
    }
}
// after
struct Provider: AppIntentTimelineProvider {
    func snapshot(for configuration: ChangeFontIntent, in context: Context) async -> DayEntry {
            return DayEntry(date: Date(), showFunFont: false)
        }
        
    func timeline(for configuration: ChangeFontIntent, in context: Context) async -> Timeline<DayEntry> {
        var entries: [DayEntry] = []
        
        let showFunFont = configuration.funFont
        
        // Generate a timeline consisting of seven entries an hour apart, starting from the current date.
        let currentDate = Date()
        for dayOffset in 0 ..< 7 {
            let entryDate = Calendar.current.date(byAdding: .day, value: dayOffset, to: currentDate)!
            let startOfDate = Calendar.current.startOfDay(for: entryDate)
            let entry = DayEntry(date: startOfDate, showFunFont: showFunFont)
            entries.append(entry)
        }
        return Timeline(entries: entries, policy: .atEnd)
    }
}

placeholder에는 바뀐게 없다.

빌드를 해보니 아이러니하게 var funFont: Bool을 한 부분에서 에러가 발생

CleanShot 2024-12-06 at 06 36 47

검색해보니

Forum에 같은 증상이 있다.

아무래도 Xcode 16에서 발생하는 문제로 보인다.

1
2
3
4
5
6
7
struct ChangeFontIntent: AppIntent, WidgetConfigurationIntent {
    static var title: LocalizedStringResource = "Fun Font"
    static var description: IntentDescription = .init(stringLiteral: "Switch to a fun font")
    
    @Parameter(title: "Fun Font", default: false)
    var funFont: Bool?
}

default: false로 했다.

그리고 관련부분을 옵셔널 바인딩으로 에러를 해결

하지만 위젯이 보이지 않는 문제가 발생했다.

깃에서 프로젝트를 다시 가져와도 안되는 문제가 발생…. 새롭게 프로젝트를 만들어서 테스트를 해본다.

CleanShot 2024-12-06 at 07 03 35

이번엔 그냥 아무것도 체크를 안하고 만들어본다.

과정은 생략

어디가 문제인가 해서 코드를 비교하다가

.disfavoredLocations([.homeScreen], for: [.systemSmall]) 여기서 애초에 홈화면에대해서 디액티브를 걸었는데 그걸 풀지않아서 생긴 문제였다…

주의하자…

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.