WidgetKit iOS 17+: App Intents로 만드는 인터랙티브 위젯
WidgetKit과 App Intents로 인터랙티브 iOS 위젯을 만드는 완전한 가이드입니다. 2026년 iOS 17+를 위한 버튼, 토글, 애니메이션 및 모범 사례.

iOS 17은 네이티브 인터랙티비티를 도입하여 WidgetKit에 혁명을 일으켰습니다. 위젯은 더 이상 정적 디스플레이가 아닙니다: 이제 앱을 열지 않고도 홈 화면에서 직접 사용자 작업에 응답할 수 있습니다. 이 중요한 발전은 App Intents 프레임워크를 기반으로 하며 매끄럽고 현대적인 사용자 경험을 제공합니다.
이 글은 프로젝트 구성부터 애니메이션과 상태 관리를 포함한 고급 패턴까지 iOS 17+ 인터랙티브 위젯의 완전한 생성을 소개합니다.
인터랙티브 위젯 아키텍처
iOS 17+ 위젯의 인터랙티비티는 App Intents 프레임워크를 통해 작동합니다. 앱을 여는 기존의 딥 링크와 달리, App Intents는 위젯에서 직접 코드를 실행한 다음 새 데이터로 디스플레이를 자동으로 새로고침할 수 있도록 합니다.
import WidgetKit
import SwiftUI
import AppIntents
// 아키텍처는 세 가지 주요 구성 요소를 기반으로 합니다:
// 1. Widget Timeline Provider - 데이터 제공
// 2. Widget View - Button/Toggle로 인터페이스 표시
// 3. App Intent - 탭 시 액션 실행
struct TaskWidget: Widget {
// 위젯의 고유 식별자
let kind: String = "TaskWidget"
var body: some WidgetConfiguration {
// 매개변수가 없는 위젯용 StaticConfiguration
StaticConfiguration(
kind: kind,
provider: TaskTimelineProvider()
) { entry in
TaskWidgetView(entry: entry)
// App Intents에 필수
.containerBackground(.fill.tertiary, for: .widget)
}
.configurationDisplayName("작업")
.description("홈 화면에서 작업을 관리하십시오.")
.supportedFamilies([.systemSmall, .systemMedium])
}
}위젯은 구성을 선언하고 데이터를 제공할 프로바이더를 지정합니다. .containerBackground 속성은 인터랙티브 위젯의 경우 iOS 17부터 필수입니다.
Timeline Provider 생성
Timeline Provider는 위젯이 언제 어떻게 새로고침될지 결정합니다. 인터랙티브 위젯의 경우 App Intents에 의해 발생한 변경 사항에도 반응해야 합니다.
import WidgetKit
import SwiftUI
// 특정 시점의 위젯 상태를 나타내는 Entry
struct TaskEntry: TimelineEntry {
let date: Date
let tasks: [Task]
// 시각적 피드백을 위한 로딩 상태
var isLoading: Bool = false
}
// 앱과 위젯 간에 공유되는 데이터 모델
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
func placeholder(in context: Context) -> TaskEntry {
TaskEntry(
date: Date(),
tasks: [
Task(id: UUID(), title: "샘플 작업", isCompleted: false, priority: .medium)
]
)
}
// 위젯 갤러리용 스냅샷
func getSnapshot(in context: Context, completion: @escaping (TaskEntry) -> Void) {
let entry = TaskEntry(
date: Date(),
tasks: TaskDataManager.shared.fetchTasks().prefix(3).map { $0 }
)
completion(entry)
}
// 새로고침 정책이 있는 완전한 timeline
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)))
// 15분 후 또는 사용자 작업 후 새로고침
let nextUpdate = Calendar.current.date(
byAdding: .minute,
value: 15,
to: Date()
) ?? Date()
let timeline = Timeline(
entries: [entry],
policy: .after(nextUpdate)
)
completion(timeline)
}
}프로바이더는 데이터에 액세스하기 위해 공유된 TaskDataManager를 사용합니다. 이 접근 방식은 메인 애플리케이션과 위젯 간의 동기화를 보장합니다.
앱과 위젯 간에 데이터를 공유하려면 프로젝트 capabilities에서 App Group을 구성해야 합니다. UserDefaults 또는 파일은 이 공유 그룹을 사용해야 합니다.
공유 데이터 매니저
애플리케이션과 위젯 간의 데이터 공유에는 App Group을 통해 액세스할 수 있는 공통 컨테이너가 필요합니다.
import Foundation
final class TaskDataManager {
// 전역 액세스를 위한 Singleton
static let shared = TaskDataManager()
// Xcode에서 구성된 App Group 식별자
private let appGroupID = "group.com.example.taskapp"
// 앱과 위젯 간에 공유된 UserDefaults
private var sharedDefaults: UserDefaults? {
UserDefaults(suiteName: appGroupID)
}
private let tasksKey = "tasks"
private init() {}
// 공유 저장소에서 작업을 가져옵니다
func fetchTasks() -> [Task] {
guard let data = sharedDefaults?.data(forKey: tasksKey),
let tasks = try? JSONDecoder().decode([Task].self, from: data) else {
return []
}
return tasks
}
// 위젯 알림과 함께 저장합니다
func saveTasks(_ tasks: [Task]) {
guard let data = try? JSONEncoder().encode(tasks) else { return }
sharedDefaults?.set(data, forKey: tasksKey)
}
// 특정 작업을 업데이트합니다
func updateTask(_ task: Task) {
var tasks = fetchTasks()
if let index = tasks.firstIndex(where: { $0.id == task.id }) {
tasks[index] = task
saveTasks(tasks)
}
}
// 완료 상태를 토글합니다
func toggleTaskCompletion(taskID: UUID) {
var tasks = fetchTasks()
if let index = tasks.firstIndex(where: { $0.id == taskID }) {
tasks[index].isCompleted.toggle()
saveTasks(tasks)
}
}
}이 매니저는 모든 영속성 로직을 캡슐화하며 애플리케이션과 위젯의 App Intents 모두에서 사용됩니다.
인터랙티비티를 위한 App Intent 생성
App Intent는 사용자가 위젯과 상호 작용할 때 실행되는 작업을 정의합니다. iOS는 이 작업을 백그라운드에서 실행한 다음 위젯을 자동으로 새로고침합니다.
import AppIntents
import WidgetKit
// 작업 상태를 토글하는 Intent
struct ToggleTaskIntent: AppIntent {
// Siri 단축어에 표시되는 제목
static var title: LocalizedStringResource = "작업 상태 토글"
// 접근성을 위한 설명
static var description = IntentDescription("작업을 완료 또는 미완료로 표시합니다.")
// 매개변수: 수정할 작업의 ID
@Parameter(title: "작업 ID")
var taskID: String
// AppIntent에 필요한 initializer
init() {}
// view에서 생성하기 위한 매개변수가 있는 initializer
init(taskID: UUID) {
self.taskID = taskID.uuidString
}
// 작업 실행
func perform() async throws -> some IntentResult {
// 문자열 ID를 UUID로 변환
guard let uuid = UUID(uuidString: taskID) else {
return .result()
}
// 작업 업데이트
TaskDataManager.shared.toggleTaskCompletion(taskID: uuid)
// 위젯 새로고침 요청
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
// 위젯 크기에 적응
@Environment(\.widgetFamily) var family
var body: some View {
VStack(alignment: .leading, spacing: 8) {
// 제목과 카운터가 있는 헤더
headerView
// 인터랙티브 버튼이 있는 작업 목록
ForEach(entry.tasks.prefix(tasksLimit)) { task in
TaskRowView(task: task)
}
Spacer(minLength: 0)
}
.padding()
}
// 크기에 따른 작업 수
private var tasksLimit: Int {
switch family {
case .systemSmall: return 2
case .systemMedium: return 3
default: return 4
}
}
private var headerView: some View {
HStack {
Text("작업")
.font(.headline)
.fontWeight(.bold)
Spacer()
// 남은 작업 수가 있는 배지
let remaining = entry.tasks.filter { !$0.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 {
// App Intent를 액션으로 하는 버튼
Button(intent: ToggleTaskIntent(taskID: task.id)) {
HStack(spacing: 12) {
// 완료 표시기
Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
.font(.title3)
.foregroundStyle(task.isCompleted ? .green : .secondary)
// 작업 제목
Text(task.title)
.font(.subheadline)
.strikethrough(task.isCompleted)
.foregroundStyle(task.isCompleted ? .secondary : .primary)
.lineLimit(1)
Spacer()
// 우선순위 표시기
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
// 명시적 상태가 있는 Toggle 전용 Intent
struct SetTaskCompletionIntent: AppIntent {
static var title: LocalizedStringResource = "작업 상태 설정"
@Parameter(title: "작업 ID")
var taskID: String
// 대상 상태: true = 완료, false = 미완료
@Parameter(title: "완료")
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: { $0.id == uuid }) {
// 상태를 명시적으로 설정 (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()
// intent가 있는 인터랙티브 toggle
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) {
// 전환 애니메이션이 있는 아이콘
Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
.font(.title3)
.foregroundStyle(task.isCompleted ? .green : .secondary)
// 변경 시 아이콘 애니메이션
.contentTransition(.symbolEffect(.replace))
Text(task.title)
.font(.subheadline)
.strikethrough(task.isCompleted)
.foregroundStyle(task.isCompleted ? .secondary : .primary)
// 텍스트 애니메이션
.contentTransition(.opacity)
Spacer()
}
.padding(.vertical, 6)
.padding(.horizontal, 10)
.background(
RoundedRectangle(cornerRadius: 8)
.fill(task.isCompleted ? Color.green.opacity(0.1) : Color.clear)
)
// 배경 애니메이션
.animation(.easeInOut(duration: 0.3), value: task.isCompleted)
}
.buttonStyle(.plain)
}
}
// 애니메이션 무효화가 있는 위젯
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("애니메이션 작업")
.description("부드러운 애니메이션이 있는 위젯.")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
// 콘텐츠 애니메이션 활성화
.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("작업")
.font(.headline.bold())
Spacer()
let completed = entry.tasks.filter(\.isCompleted).count
let total = entry.tasks.count
// 애니메이션 진행률
Text("\(completed)/\(total)")
.font(.caption.bold())
.foregroundStyle(.secondary)
.contentTransition(.numericText())
}
}
}.symbolEffect(.replace) 및 .numericText() 애니메이션은 상태 간 부드러운 전환을 만들어 사용자 경험을 크게 향상시킵니다.
AppIntentConfiguration으로 구성 가능한 위젯
사용자가 사용자 정의할 수 있는 위젯(필터, 카테고리)의 경우 AppIntentConfiguration이 StaticConfiguration을 대체합니다.
import WidgetKit
import SwiftUI
import AppIntents
// 사용자에게 노출되는 구성
struct TaskWidgetConfigurationIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource = "작업 구성"
static var description = IntentDescription("작업 표시를 사용자 정의합니다.")
// 우선순위별 필터
@Parameter(title: "우선순위", default: .all)
var priorityFilter: PriorityFilter
// 완료된 작업 표시
@Parameter(title: "완료된 항목 표시", default: true)
var showCompleted: Bool
// 최대 작업 수
@Parameter(title: "작업 수", default: 3)
var maxTasks: Int
}
// 우선순위 필터용 Enum
enum PriorityFilter: String, AppEnum {
case all
case high
case medium
case low
static var typeDisplayRepresentation: TypeDisplayRepresentation = "우선순위"
static var caseDisplayRepresentations: [PriorityFilter: DisplayRepresentation] = [
.all: "전체",
.high: "높음",
.medium: "중간",
.low: "낮음"
]
}
// 구성에 맞춰진 프로바이더
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))
}
// 구성 필터 적용
private func filteredTasks(for config: TaskWidgetConfigurationIntent) -> [Task] {
var tasks = TaskDataManager.shared.fetchTasks()
// 우선순위로 필터링
if config.priorityFilter != .all {
let priority = Task.Priority(rawValue: config.priorityFilter.rawValue) ?? .medium
tasks = tasks.filter { $0.priority == priority }
}
// 필요시 완료된 항목 필터링
if !config.showCompleted {
tasks = tasks.filter { !$0.isCompleted }
}
// 수량 제한
return Array(tasks.prefix(config.maxTasks))
}
}
// 사용자 구성이 있는 위젯
struct ConfigurableTaskWidget: Widget {
let kind: String = "ConfigurableTaskWidget"
var body: some WidgetConfiguration {
// 구성 가능한 위젯용 AppIntentConfiguration
AppIntentConfiguration(
kind: kind,
intent: TaskWidgetConfigurationIntent.self,
provider: ConfigurableTaskProvider()
) { entry in
TaskWidgetView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
}
.configurationDisplayName("맞춤 작업")
.description("작업을 필터링하고 사용자 정의합니다.")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
}
}사용자는 이제 길게 누르기를 통해 위젯을 구성할 수 있으며, 애플리케이션에 추가 코드 없이 개인화된 경험을 제공받습니다.
오류 처리 및 로딩 상태
좋은 UX는 상호 작용 중 오류 사례와 중간 상태를 처리해야 합니다.
import AppIntents
import WidgetKit
struct ToggleTaskWithFeedbackIntent: AppIntent {
static var title: LocalizedStringResource = "피드백이 있는 작업 토글"
@Parameter(title: "작업 ID")
var taskID: String
init() {}
init(taskID: UUID) {
self.taskID = taskID.uuidString
}
func perform() async throws -> some IntentResult & ReturnsValue<Bool> {
guard let uuid = UUID(uuidString: taskID) else {
// 무음 실패 반환
return .result(value: false)
}
// 비동기 작업 시뮬레이션 (예: 서버 동기화)
do {
try await Task.sleep(for: .milliseconds(100))
TaskDataManager.shared.toggleTaskCompletion(taskID: uuid)
WidgetCenter.shared.reloadTimelines(ofKind: "TaskWidget")
return .result(value: true)
} catch {
// 오류: 위젯을 업데이트하지 않음
return .result(value: false)
}
}
}
// 로딩 상태가 있는 View
struct TaskRowWithLoadingView: View {
let task: Task
@State private var isLoading = false
var body: some View {
Button(intent: ToggleTaskIntent(taskID: task.id)) {
HStack(spacing: 12) {
// 조건부 표시기
Group {
if isLoading {
ProgressView()
.scaleEffect(0.8)
} else {
Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
.foregroundStyle(task.isCompleted ? .green : .secondary)
}
}
.frame(width: 24, height: 24)
Text(task.title)
.font(.subheadline)
.strikethrough(task.isCompleted)
Spacer()
}
.padding(.vertical, 6)
.padding(.horizontal, 10)
.background(Color(.systemBackground).opacity(0.5))
.cornerRadius(8)
}
.buttonStyle(.plain)
.disabled(isLoading)
.opacity(isLoading ? 0.6 : 1.0)
}
}즉각적인 시각적 피드백(불투명도 감소, 로딩 표시기)은 사용자의 작업이 등록되었음을 알려줍니다.
모범 사례 및 최적화
여러 패턴은 성능이 뛰어나고 신뢰할 수 있는 인터랙티브 위젯을 보장합니다.
import WidgetKit
import SwiftUI
// 1. 수정 후 항상 캐시 무효화
final class WidgetRefreshManager {
static func refreshAllWidgets() {
// 앱의 모든 위젯 새로고침
WidgetCenter.shared.reloadAllTimelines()
}
static func refreshWidget(kind: String) {
// 특정 위젯 새로고침
WidgetCenter.shared.reloadTimelines(ofKind: kind)
}
// 데이터 수정 후 앱에서 호출
static func notifyDataChanged() {
Task { @MainActor in
refreshAllWidgets()
}
}
}
// 2. View 복잡도 제한
struct OptimizedWidgetView: View {
let entry: TaskEntry
var body: some View {
// GeometryReader 없이 단순한 view 선호
VStack(alignment: .leading, spacing: 8) {
ForEach(entry.tasks.prefix(3)) { task in
// 가벼운 컴포넌트
minimalTaskRow(task)
}
}
.padding()
}
// 성능을 위한 최소 view
@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. 단순한 상태에는 @AppStorage 사용
struct QuickSettingsWidgetView: View {
// 공유 UserDefaults에 직접 액세스
@AppStorage("showCompletedTasks", store: UserDefaults(suiteName: "group.com.example.app"))
private var showCompleted = true
var body: some View {
// 상태는 새로고침 사이에 유지됩니다
Text(showCompleted ? "모두 표시 중" : "완료된 항목 숨기는 중")
}
}
// 4. 프로바이더에서 데이터 사전 계산
struct OptimizedTaskEntry: TimelineEntry {
let date: Date
let tasks: [Task]
// 사전 계산된 데이터
let completedCount: Int
let pendingCount: Int
let highPriorityCount: Int
init(date: Date, tasks: [Task]) {
self.date = date
self.tasks = tasks
// 계산은 한 번만 수행됩니다
self.completedCount = tasks.filter(\.isCompleted).count
self.pendingCount = tasks.filter { !$0.isCompleted }.count
self.highPriorityCount = tasks.filter { $0.priority == .high && !$0.isCompleted }.count
}
}이러한 최적화는 배터리를 과도하게 소비하지 않는 반응형 위젯을 보장합니다.
디버깅에는 Xcode의 위젯 스킴을 사용하십시오. 캔버스 미리보기를 통해 장치 설치 없이 다양한 크기와 상태를 테스트할 수 있습니다.
결론
App Intents가 있는 WidgetKit iOS 17+은 위젯을 iOS 애플리케이션의 진정한 인터랙티브 확장으로 변환합니다. 이 선언적 아키텍처는 네이티브하고 매끄러운 사용자 경험을 제공하면서 개발을 크게 단순화합니다.
iOS 17+ 인터랙티브 위젯 체크리스트
- ✅ 데이터 공유를 위한 App Group 구성
- ✅ 적절한 새로고침이 있는 Timeline Provider 생성
- ✅ 각 작업에 대한 App Intents 구현
- ✅ 인터랙티비티를 위해
Button(intent:)또는Toggle(intent:)사용 - ✅ 수정 후
WidgetCenter.shared.reloadTimelines호출 - ✅ iOS 17+에 필수인
.containerBackground추가 - ✅ 부드러운 전환 애니메이션 구현
- ✅ 로딩 및 오류 상태 처리
- ✅ 배터리 성능을 위해 view 최적화
- ✅ 지원되는 모든 위젯 크기에서 테스트
연습을 시작하세요!
면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.
태그
공유
관련 기사

App Intents와 Siri Shortcuts: iOS 2026 고급 자동화
iOS 18+를 위한 App Intents와 Siri Shortcuts 완전 가이드입니다. 맞춤형 Siri 액션 구축, Apple Intelligence 통합, 2026년 Swift 앱 자동화를 다룹니다.

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

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