WidgetKit iOS 17-26: App Intents로 인터랙티브 위젯 구축하기
WidgetKit과 App Intents로 인터랙티브 iOS 위젯을 만드는 완전한 가이드입니다. 버튼, 토글, 애니메이션, iOS 26 Liquid Glass 머티리얼, WWDC 2026 업데이트를 다룹니다.

iOS 17은 WidgetKit에 네이티브 인터랙티브 기능을 도입하여, 앱을 열지 않고도 홈 화면에서 직접 사용자 동작에 응답할 수 있게 되었습니다. iOS 26에서는 Liquid Glass 머티리얼과 악센트 렌더링 모드로 이를 개선했습니다. 이 가이드에서는 iOS 17부터 iOS 26까지의 인터랙티브 위젯 개발을 다룹니다.
프로젝트 설정부터 애니메이션과 상태 관리를 포함한 고급 패턴, 최신 WWDC 2026 App Intents 기능까지 iOS 17-26 인터랙티브 위젯 생성을 다룹니다.
iOS 17+의 인터랙티브 위젯 아키텍처
iOS 17+ 위젯 인터랙티브 기능은 App Intents 프레임워크를 통해 작동합니다. 앱을 여는 기존의 딥 링크와 달리, App Intents는 위젯에서 직접 코드를 실행한 다음 새 데이터로 디스플레이를 자동으로 새로고침합니다.
import WidgetKit
import SwiftUI
import AppIntents
// The architecture relies on three main components:
// 1. Widget Timeline Provider - supplies the data
// 2. Widget View - displays the interface with Button/Toggle
// 3. App Intent - executes the action on tap
struct TaskWidget: Widget {
// Unique widget identifier
let kind: String = "TaskWidget"
var body: some WidgetConfiguration {
// StaticConfiguration for widgets without parameters
StaticConfiguration(
kind: kind,
provider: TaskTimelineProvider()
) { entry in
TaskWidgetView(entry: entry)
// Required for App Intents since iOS 17
.containerBackground(.fill.tertiary, for: .widget)
}
.configurationDisplayName("Tasks")
.description("Manage your tasks from the home screen.")
.supportedFamilies([.systemSmall, .systemMedium])
}
}위젯은 구성을 선언하고 데이터를 제공할 프로바이더를 지정합니다. .containerBackground 속성은 iOS 17 이상의 인터랙티브 위젯에 필수이며, iOS 26에서는 적절한 경우 Liquid Glass 스타일링이 자동으로 적용됩니다.
Timeline Provider 생성
Timeline Provider는 위젯이 언제 어떻게 새로고침될지 결정합니다. 인터랙티브 위젯의 경우 App Intents에 의해 트리거된 변경 사항에도 반응해야 합니다.
import WidgetKit
import SwiftUI
// Entry representing widget state at a given moment
struct TaskEntry: TimelineEntry {
let date: Date
let tasks: [Task]
// Loading state for visual feedback
var isLoading: Bool = false
}
// Data model shared between app and widget
struct Task: Identifiable, Codable {
let id: UUID
var title: String
var isCompleted: Bool
var priority: Priority
enum Priority: String, Codable {
case low, medium, high
}
}
struct TaskTimelineProvider: TimelineProvider {
// Placeholder displayed during initial loading
func placeholder(in context: Context) -> TaskEntry {
TaskEntry(
date: Date(),
tasks: [
Task(id: UUID(), title: "Sample task", isCompleted: false, priority: .medium)
]
)
}
// Snapshot for widget gallery
func getSnapshot(in context: Context, completion: @escaping (TaskEntry) -> Void) {
let entry = TaskEntry(
date: Date(),
tasks: TaskDataManager.shared.fetchTasks().prefix(3).map { /bin/bash }
)
completion(entry)
}
// Complete timeline with refresh policy
func getTimeline(in context: Context, completion: @escaping (Timeline<TaskEntry>) -> Void) {
let tasks = TaskDataManager.shared.fetchTasks()
let entry = TaskEntry(date: Date(), tasks: Array(tasks.prefix(3)))
// Refresh in 15 minutes or after user action
let nextUpdate = Calendar.current.date(
byAdding: .minute,
value: 15,
to: Date()
) ?? Date()
let timeline = Timeline(
entries: [entry],
policy: .after(nextUpdate)
)
completion(timeline)
}
}프로바이더는 공유된 TaskDataManager를 사용하여 데이터에 접근합니다. 이 접근 방식은 메인 애플리케이션과 위젯 간의 동기화를 보장합니다. 관련 상태 관리 패턴은 SwiftUI @Observable vs @State를 참조하십시오.
앱과 위젯 간에 데이터를 공유하려면 프로젝트 capabilities에서 App Group을 구성해야 합니다. UserDefaults 또는 파일은 이 공유 그룹을 사용해야 합니다.
공유 데이터 매니저
애플리케이션과 위젯 간의 데이터 공유에는 App Group을 통해 접근할 수 있는 공통 컨테이너가 필요합니다.
import Foundation
final class TaskDataManager {
// Singleton for global access
static let shared = TaskDataManager()
// App Group identifier configured in Xcode
private let appGroupID = "group.com.example.taskapp"
// UserDefaults shared between app and widget
private var sharedDefaults: UserDefaults? {
UserDefaults(suiteName: appGroupID)
}
private let tasksKey = "tasks"
private init() {}
// Fetch tasks from shared storage
func fetchTasks() -> [Task] {
guard let data = sharedDefaults?.data(forKey: tasksKey),
let tasks = try? JSONDecoder().decode([Task].self, from: data) else {
return []
}
return tasks
}
// Save with widget notification
func saveTasks(_ tasks: [Task]) {
guard let data = try? JSONEncoder().encode(tasks) else { return }
sharedDefaults?.set(data, forKey: tasksKey)
}
// Update a specific task
func updateTask(_ task: Task) {
var tasks = fetchTasks()
if let index = tasks.firstIndex(where: { /bin/bash.id == task.id }) {
tasks[index] = task
saveTasks(tasks)
}
}
// Toggle completed state
func toggleTaskCompletion(taskID: UUID) {
var tasks = fetchTasks()
if let index = tasks.firstIndex(where: { /bin/bash.id == taskID }) {
tasks[index].isCompleted.toggle()
saveTasks(tasks)
}
}
}이 매니저는 모든 영속성 로직을 캡슐화하며 애플리케이션과 위젯의 App Intents 모두에서 사용됩니다.
인터랙티브 기능을 위한 App Intent 생성
App Intent는 사용자가 위젯과 상호 작용할 때 실행되는 동작을 정의합니다. iOS는 이 동작을 백그라운드에서 실행한 다음 위젯을 자동으로 새로고침합니다.
import AppIntents
import WidgetKit
// Intent to toggle task state
struct ToggleTaskIntent: AppIntent {
// Title displayed in Siri shortcuts
static var title: LocalizedStringResource = "Toggle task state"
// Description for accessibility
static var description = IntentDescription("Marks a task as completed or not completed.")
// Parameter: ID of task to modify
@Parameter(title: "Task ID")
var taskID: String
// Required initializer for AppIntent
init() {}
// Initializer with parameter for view creation
init(taskID: UUID) {
self.taskID = taskID.uuidString
}
// Action execution
func perform() async throws -> some IntentResult {
// Convert string ID to UUID
guard let uuid = UUID(uuidString: taskID) else {
return .result()
}
// Update the task
TaskDataManager.shared.toggleTaskCompletion(taskID: uuid)
// Request widget refresh
WidgetCenter.shared.reloadTimelines(ofKind: "TaskWidget")
return .result()
}
}WidgetCenter.shared.reloadTimelines 호출은 동작 후 위젯을 즉시 새로고침하여 즉각적인 시각적 피드백을 보장합니다.
iOS 면접 준비가 되셨나요?
인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.
인터랙티브 버튼이 있는 위젯 View
위젯 view는 intent를 동작으로 하는 표준 SwiftUI Button 컴포넌트를 사용합니다. iOS 17+는 이러한 상호 작용을 자동으로 가로채서 App Intent를 실행합니다.
import SwiftUI
import WidgetKit
struct TaskWidgetView: View {
let entry: TaskEntry
// Adapt to widget size
@Environment(.widgetFamily) var family
var body: some View {
VStack(alignment: .leading, spacing: 8) {
// Header with title and counter
headerView
// Task list with interactive buttons
ForEach(entry.tasks.prefix(tasksLimit)) { task in
TaskRowView(task: task)
}
Spacer(minLength: 0)
}
.padding()
}
// Number of tasks based on size
private var tasksLimit: Int {
switch family {
case .systemSmall: return 2
case .systemMedium: return 3
default: return 4
}
}
private var headerView: some View {
HStack {
Text("Tasks")
.font(.headline)
.fontWeight(.bold)
Spacer()
// Badge with remaining task count
let remaining = entry.tasks.filter { !/bin/bash.isCompleted }.count
Text("\(remaining)")
.font(.caption.bold())
.foregroundStyle(.white)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(remaining > 0 ? Color.orange : Color.green)
.clipShape(Capsule())
}
}
}
struct TaskRowView: View {
let task: Task
var body: some View {
// Button with App Intent as action
Button(intent: ToggleTaskIntent(taskID: task.id)) {
HStack(spacing: 12) {
// Completion indicator
Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
.font(.title3)
.foregroundStyle(task.isCompleted ? .green : .secondary)
// Task title
Text(task.title)
.font(.subheadline)
.strikethrough(task.isCompleted)
.foregroundStyle(task.isCompleted ? .secondary : .primary)
.lineLimit(1)
Spacer()
// Priority indicator
priorityIndicator
}
.padding(.vertical, 6)
.padding(.horizontal, 10)
.background(Color(.systemBackground).opacity(0.5))
.cornerRadius(8)
}
.buttonStyle(.plain)
}
@ViewBuilder
private var priorityIndicator: some View {
switch task.priority {
case .high:
Image(systemName: "exclamationmark.circle.fill")
.foregroundStyle(.red)
case .medium:
Image(systemName: "minus.circle.fill")
.foregroundStyle(.orange)
case .low:
EmptyView()
}
}
}Button(intent:) 구문은 버튼을 App Intent에 직접 연결합니다. 탭 시 iOS는 perform()을 실행한 다음 위젯을 자동으로 새로고침합니다.
위젯을 위한 인터랙티브 Toggle
켜기/끄기 유형 동작의 경우 Toggle 컴포넌트는 네이티브 iOS 스타일링으로 버튼에 대한 대안을 제공합니다.
import SwiftUI
import AppIntents
// Specific intent for Toggle with explicit state
struct SetTaskCompletionIntent: AppIntent {
static var title: LocalizedStringResource = "Set task state"
@Parameter(title: "Task ID")
var taskID: String
// Target state: true = completed, false = not completed
@Parameter(title: "Completed")
var isCompleted: Bool
init() {}
init(taskID: UUID, isCompleted: Bool) {
self.taskID = taskID.uuidString
self.isCompleted = isCompleted
}
func perform() async throws -> some IntentResult {
guard let uuid = UUID(uuidString: taskID) else {
return .result()
}
var tasks = TaskDataManager.shared.fetchTasks()
if let index = tasks.firstIndex(where: { /bin/bash.id == uuid }) {
// Set state explicitly (not toggle)
tasks[index].isCompleted = isCompleted
TaskDataManager.shared.saveTasks(tasks)
}
WidgetCenter.shared.reloadTimelines(ofKind: "TaskWidget")
return .result()
}
}
struct TaskToggleRowView: View {
let task: Task
var body: some View {
HStack {
Text(task.title)
.font(.subheadline)
.strikethrough(task.isCompleted)
Spacer()
// Interactive toggle with intent
Toggle(
isOn: task.isCompleted,
intent: SetTaskCompletionIntent(
taskID: task.id,
isCompleted: !task.isCompleted
)
)
.toggleStyle(.switch)
.labelsHidden()
}
.padding(.vertical, 4)
}
}Toggle은 이진 상태에 대해 더 직관적인 상호 작용을 제공하며 iOS 디자인에 자연스럽게 통합됩니다.
위젯은 알림, 시트 또는 내비게이션을 표시할 수 없습니다. 모든 동작은 자체적으로 완결되어야 하며 보이는 상태를 직접 업데이트해야 합니다.
새로고침 애니메이션과 전환
iOS 17+는 동작 후 위젯 새로고침 중 전환 애니메이션을 허용합니다. .contentTransition 수정자가 이러한 애니메이션을 제어합니다.
import SwiftUI
import WidgetKit
struct AnimatedTaskRowView: View {
let task: Task
var body: some View {
Button(intent: ToggleTaskIntent(taskID: task.id)) {
HStack(spacing: 12) {
// Icon with transition animation
Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
.font(.title3)
.foregroundStyle(task.isCompleted ? .green : .secondary)
// Icon animation on change
.contentTransition(.symbolEffect(.replace))
Text(task.title)
.font(.subheadline)
.strikethrough(task.isCompleted)
.foregroundStyle(task.isCompleted ? .secondary : .primary)
// Text animation
.contentTransition(.opacity)
Spacer()
}
.padding(.vertical, 6)
.padding(.horizontal, 10)
.background(
RoundedRectangle(cornerRadius: 8)
.fill(task.isCompleted ? Color.green.opacity(0.1) : Color.clear)
)
// Background animation
.animation(.easeInOut(duration: 0.3), value: task.isCompleted)
}
.buttonStyle(.plain)
}
}
// Widget with animated invalidation
struct AnimatedTaskWidget: Widget {
let kind: String = "AnimatedTaskWidget"
var body: some WidgetConfiguration {
StaticConfiguration(
kind: kind,
provider: TaskTimelineProvider()
) { entry in
AnimatedTaskWidgetView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
}
.configurationDisplayName("Animated Tasks")
.description("Widgets with smooth animations.")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
// Enable content animations
.contentMarginsDisabled()
}
}
struct AnimatedTaskWidgetView: View {
let entry: TaskEntry
var body: some View {
VStack(alignment: .leading, spacing: 8) {
headerView
ForEach(entry.tasks) { task in
AnimatedTaskRowView(task: task)
}
Spacer(minLength: 0)
}
.padding()
}
private var headerView: some View {
HStack {
Text("Tasks")
.font(.headline.bold())
Spacer()
let completed = entry.tasks.filter(.isCompleted).count
let total = entry.tasks.count
// Animated progress
Text("\(completed)/\(total)")
.font(.caption.bold())
.foregroundStyle(.secondary)
.contentTransition(.numericText())
}
}
}.symbolEffect(.replace)와 .numericText() 애니메이션은 상태 간 부드러운 전환을 생성하여 사용자 경험을 크게 향상시킵니다.
iOS 26의 Liquid Glass와 악센트 렌더링
iOS 26에서는 Liquid Glass 머티리얼과 위젯의 악센트 렌더링 모드가 도입되었습니다. 이러한 시각적 업데이트는 .containerBackground를 사용하면 자동으로 적용되지만, 개발자는 렌더링 동작을 제어할 수 있습니다.
import SwiftUI
import WidgetKit
struct ModernTaskWidgetView: View {
let entry: TaskEntry
var body: some View {
VStack(alignment: .leading, spacing: 8) {
ForEach(entry.tasks.prefix(3)) { task in
TaskRowView(task: task)
}
}
.padding()
// iOS 26: Control accented rendering for images
.widgetAccentedRenderingMode(.fullColor)
}
}
// Extra large portrait family (iOS 26+)
struct LargePortraitTaskWidget: Widget {
let kind: String = "LargePortraitTaskWidget"
var body: some WidgetConfiguration {
StaticConfiguration(
kind: kind,
provider: TaskTimelineProvider()
) { entry in
LargePortraitTaskWidgetView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
}
.configurationDisplayName("Task Overview")
.description("Full task overview with extra space.")
// New in iOS 26: system extra large portrait
.supportedFamilies([.systemExtraLarge])
}
}
struct LargePortraitTaskWidgetView: View {
let entry: TaskEntry
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("All Tasks")
.font(.title2.bold())
// More space for task details
ForEach(entry.tasks) { task in
DetailedTaskRowView(task: task)
}
Spacer(minLength: 0)
}
.padding()
}
}
struct DetailedTaskRowView: View {
let task: Task
var body: some View {
Button(intent: ToggleTaskIntent(taskID: task.id)) {
VStack(alignment: .leading, spacing: 4) {
HStack {
Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
.foregroundStyle(task.isCompleted ? .green : .secondary)
Text(task.title)
.font(.headline)
.strikethrough(task.isCompleted)
}
Text("Priority: \(task.priority.rawValue.capitalized)")
.font(.caption)
.foregroundStyle(.secondary)
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color(.systemBackground).opacity(0.3))
.cornerRadius(12)
}
.buttonStyle(.plain)
}
}.widgetAccentedRenderingMode(.fullColor) 수정자는 악센트 환경에서 이미지 색상을 유지합니다. visionOS 26에서 처음 등장한 .systemExtraLarge 패밀리는 이제 iOS와 iPadOS에서도 사용할 수 있습니다.
WWDC 2026 App Intents: 성능을 위한 EntityCollection
WWDC 2026에서는 다수의 엔티티를 효율적으로 처리하기 위한 EntityCollection이 도입되었습니다. Intent 실행 전에 모든 엔티티를 해결하는 대신, 식별자만 전달됩니다.
import AppIntents
import WidgetKit
// Mark multiple tasks complete at once - efficiently
struct BatchCompleteTasksIntent: AppIntent {
static var title: LocalizedStringResource = "Complete multiple tasks"
// EntityCollection passes IDs only, not resolved entities
@Parameter(title: "Tasks")
var tasks: EntityCollection<TaskEntity>
init() {}
init(taskIDs: [UUID]) {
// Create collection from IDs
self.tasks = EntityCollection(taskIDs.map { TaskEntity(id: /bin/bash) })
}
func perform() async throws -> some IntentResult {
// Access IDs directly without resolution overhead
let taskIDs = tasks.identifiers
var allTasks = TaskDataManager.shared.fetchTasks()
for id in taskIDs {
if let uuid = UUID(uuidString: id),
let index = allTasks.firstIndex(where: { /bin/bash.id == uuid }) {
allTasks[index].isCompleted = true
}
}
TaskDataManager.shared.saveTasks(allTasks)
WidgetCenter.shared.reloadTimelines(ofKind: "TaskWidget")
return .result()
}
}
// Entity conformance for TaskEntity
struct TaskEntity: AppEntity {
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Task"
static var defaultQuery = TaskEntityQuery()
var id: UUID
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(title: "Task \(id.uuidString.prefix(8))")
}
}
struct TaskEntityQuery: EntityQuery {
func entities(for identifiers: [UUID]) async throws -> [TaskEntity] {
identifiers.map { TaskEntity(id: /bin/bash) }
}
}이 패턴은 일괄 작업에 필수적입니다. Apple의 내부 벤치마크에 따르면 1000장의 사진에 태그 지정하는 작업이 EntityCollection으로 "거의 즉시"로 개선되었습니다.
진행률 표시가 있는 장시간 실행 Intent
WWDC 2026에서는 표준 30초 제한을 초과하는 작업을 위한 LongRunningIntent도 도입되었으며, 진행률은 Live Activity로 표시됩니다.
import AppIntents
import WidgetKit
struct SyncAllTasksIntent: LongRunningIntent, CancellableIntent {
static var title: LocalizedStringResource = "Sync all tasks"
func perform() async throws -> some IntentResult & ProvidesDialog {
let result = try await performBackgroundTask {
let tasks = TaskDataManager.shared.fetchTasks()
progress.totalUnitCount = Int64(tasks.count)
for (index, task) in tasks.enumerated() {
// Check for cancellation
try Task.checkCancellation()
// Simulate server sync
try await Task.sleep(for: .milliseconds(100))
progress.completedUnitCount = Int64(index + 1)
}
return "Synced \(tasks.count) tasks"
} onCancel: { reason in
// Cleanup based on cancellation reason
switch reason {
case .user:
print("User cancelled sync")
case .timeout:
print("System timeout")
case .systemReclaim:
print("System reclaimed resources")
@unknown default:
break
}
}
WidgetCenter.shared.reloadTimelines(ofKind: "TaskWidget")
return .result(dialog: "\(result)")
}
}진행률은 Live Activity로 표시되며, CancellableIntent는 취소 이유를 제공하여 적절한 정리 작업이 가능합니다. Swift 동시성 패턴에 대한 자세한 내용은 Swift Structured Concurrency를 참조하십시오.
AppIntentConfiguration으로 구성 가능한 위젯
사용자가 사용자 정의할 수 있는 위젯(필터, 카테고리)의 경우 AppIntentConfiguration이 StaticConfiguration을 대체합니다.
import WidgetKit
import SwiftUI
import AppIntents
// Configuration exposed to user
struct TaskWidgetConfigurationIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource = "Task configuration"
static var description = IntentDescription("Customize task display.")
// Filter by priority
@Parameter(title: "Priority", default: .all)
var priorityFilter: PriorityFilter
// Show completed tasks
@Parameter(title: "Show completed", default: true)
var showCompleted: Bool
// Maximum number of tasks
@Parameter(title: "Task count", default: 3)
var maxTasks: Int
}
// Enum for priority filter
enum PriorityFilter: String, AppEnum {
case all
case high
case medium
case low
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Priority"
static var caseDisplayRepresentations: [PriorityFilter: DisplayRepresentation] = [
.all: "All",
.high: "High",
.medium: "Medium",
.low: "Low"
]
}
// Provider adapted to configuration
struct ConfigurableTaskProvider: AppIntentTimelineProvider {
typealias Entry = TaskEntry
typealias Intent = TaskWidgetConfigurationIntent
func placeholder(in context: Context) -> TaskEntry {
TaskEntry(date: Date(), tasks: [])
}
func snapshot(for configuration: TaskWidgetConfigurationIntent, in context: Context) async -> TaskEntry {
let tasks = filteredTasks(for: configuration)
return TaskEntry(date: Date(), tasks: tasks)
}
func timeline(for configuration: TaskWidgetConfigurationIntent, in context: Context) async -> Timeline<TaskEntry> {
let tasks = filteredTasks(for: configuration)
let entry = TaskEntry(date: Date(), tasks: tasks)
let nextUpdate = Date().addingTimeInterval(15 * 60)
return Timeline(entries: [entry], policy: .after(nextUpdate))
}
// Apply configuration filters
private func filteredTasks(for config: TaskWidgetConfigurationIntent) -> [Task] {
var tasks = TaskDataManager.shared.fetchTasks()
// Filter by priority
if config.priorityFilter != .all {
let priority = Task.Priority(rawValue: config.priorityFilter.rawValue) ?? .medium
tasks = tasks.filter { /bin/bash.priority == priority }
}
// Filter completed if needed
if !config.showCompleted {
tasks = tasks.filter { !/bin/bash.isCompleted }
}
// Limit count
return Array(tasks.prefix(config.maxTasks))
}
}
// Widget with user configuration
struct ConfigurableTaskWidget: Widget {
let kind: String = "ConfigurableTaskWidget"
var body: some WidgetConfiguration {
// AppIntentConfiguration for configurable widgets
AppIntentConfiguration(
kind: kind,
intent: TaskWidgetConfigurationIntent.self,
provider: ConfigurableTaskProvider()
) { entry in
TaskWidgetView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
}
.configurationDisplayName("Custom Tasks")
.description("Filter and customize your tasks.")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
}
}사용자는 길게 누르기를 통해 위젯을 구성할 수 있으며, 애플리케이션에 추가 코드 없이 개인화된 경험을 제공받습니다. App Intents와 Siri 통합에 대한 자세한 내용은 App Intents 2.0과 Siri 단축어를 참조하십시오.
모범 사례와 최적화
여러 패턴이 성능이 뛰어나고 신뢰할 수 있는 인터랙티브 위젯을 보장합니다.
import WidgetKit
import SwiftUI
// 1. Always invalidate cache after modification
final class WidgetRefreshManager {
static func refreshAllWidgets() {
// Refresh all app widgets
WidgetCenter.shared.reloadAllTimelines()
}
static func refreshWidget(kind: String) {
// Refresh specific widget
WidgetCenter.shared.reloadTimelines(ofKind: kind)
}
// Call from app after data modification
static func notifyDataChanged() {
Task { @MainActor in
refreshAllWidgets()
}
}
}
// 2. Limit view complexity
struct OptimizedWidgetView: View {
let entry: TaskEntry
var body: some View {
// Prefer simple views without GeometryReader
VStack(alignment: .leading, spacing: 8) {
ForEach(entry.tasks.prefix(3)) { task in
// Lightweight components
minimalTaskRow(task)
}
}
.padding()
}
// Minimal view for performance
@ViewBuilder
private func minimalTaskRow(_ task: Task) -> some View {
Button(intent: ToggleTaskIntent(taskID: task.id)) {
HStack {
Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
Text(task.title)
.lineLimit(1)
}
}
.buttonStyle(.plain)
}
}
// 3. Use @AppStorage for simple state
struct QuickSettingsWidgetView: View {
// Direct access to shared UserDefaults
@AppStorage("showCompletedTasks", store: UserDefaults(suiteName: "group.com.example.app"))
private var showCompleted = true
var body: some View {
// State persists between refreshes
Text(showCompleted ? "Showing all" : "Hiding completed")
}
}
// 4. Pre-calculate data in provider
struct OptimizedTaskEntry: TimelineEntry {
let date: Date
let tasks: [Task]
// Pre-calculated data
let completedCount: Int
let pendingCount: Int
let highPriorityCount: Int
init(date: Date, tasks: [Task]) {
self.date = date
self.tasks = tasks
// Calculations performed once
self.completedCount = tasks.filter(.isCompleted).count
self.pendingCount = tasks.filter { !/bin/bash.isCompleted }.count
self.highPriorityCount = tasks.filter { /bin/bash.priority == .high && !/bin/bash.isCompleted }.count
}
}이러한 최적화는 배터리를 과도하게 소비하지 않는 반응형 위젯을 보장합니다.
디버깅에는 Xcode의 위젯 스킴을 사용하십시오. 캔버스 미리보기를 통해 장치 설치 없이 다양한 크기와 상태를 테스트할 수 있습니다.
Sources
- WidgetKit Documentation - Apple 공식 WidgetKit 레퍼런스
- App Intents Framework - 완전한 App Intents API 문서
- WWDC 2026: Discover new capabilities in the App Intents framework - EntityCollection, LongRunningIntent, SyncableEntity
- Human Interface Guidelines: Materials - Liquid Glass 디자인 가이드
인터랙티브 iOS 위젯 개발 체크리스트
App Intents가 포함된 WidgetKit은 위젯을 iOS 애플리케이션의 진정한 인터랙티브 확장으로 변환합니다. iOS 17에서 도입된 아키텍처는 iOS 26까지 안정적이며, 시각적 개선과 성능 기능이 추가되었습니다.
구현 체크리스트
- 앱과 위젯 확장 간의 데이터 공유를 위해 App Group 구성
- 적절한 새로고침 정책을 가진 Timeline Provider 생성
- 각 사용자 동작에 대한 App Intents 구현
- 인터랙티브 기능을 위해
Button(intent:)또는Toggle(intent:)사용 - 데이터 변경 후
WidgetCenter.shared.reloadTimelines호출 - iOS 17+ 호환성을 위해 필수
.containerBackground추가 - iOS 26 Liquid Glass 지원을 위해
.widgetAccentedRenderingMode사용 .contentTransition으로 부드러운 전환 애니메이션 구현- 시각적 피드백을 위한 로딩 및 오류 상태 처리
- 일괄 작업에서 해결 오버헤드를 피하기 위해
EntityCollection사용 .systemExtraLarge를 포함한 모든 지원 위젯 크기에서 테스트
연습을 시작하세요!
면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.
iOS 코드의 버그를 찾을 수 있나요
실제 코드 한 조각, 숨은 버그 하나, 하루 한 번. 계정 없이 바로 도전할 수 있습니다.

작성자
Anthony Fillion-MailletSharpSkill 창업자
10년 이상 풀스택 개발을 해왔습니다. SharpSkill을 운영하며 이곳에 게시되는 모든 내용에 책임을 집니다.
2026년 8월 24일 업데이트
태그
공유
관련 기사

App Intents 2.0과 Siri Shortcuts: iOS 27 자동화 가이드
iOS 27을 위한 App Intents 2.0과 Siri Shortcuts 완전 가이드입니다. 스트리밍 응답, 멀티턴 대화, View Annotations 구축, Foundation Models와의 통합을 다룹니다.

Swift에서 Combine vs async/await: 점진적 마이그레이션 패턴
Swift에서 Combine에서 async/await로 마이그레이션하는 완전한 가이드: 점진적 전략, 브리징 패턴, iOS 코드베이스의 패러다임 공존.

2026년 iOS 접근성 면접 질문: VoiceOver와 Dynamic Type
iOS 면접 대비를 위한 핵심 접근성 질문: VoiceOver, Dynamic Type, 시맨틱 traits, 접근성 감사.