WidgetKit iOS 17+: Interaktywne Widgety z App Intents
Kompletny przewodnik tworzenia interaktywnych widgetów iOS z WidgetKit i App Intents. Przyciski, przełączniki, animacje i najlepsze praktyki dla iOS 17+ w 2026 roku.

iOS 17 zrewolucjonował WidgetKit, wprowadzając natywną interaktywność. Widgety nie są już statycznymi wyświetleniami: teraz mogą reagować na działania użytkownika bezpośrednio z ekranu głównego, bez otwierania aplikacji. Ta ważna ewolucja opiera się na frameworku App Intents i zapewnia płynne, nowoczesne doświadczenie użytkownika.
Artykuł przedstawia kompletne tworzenie interaktywnych widgetów iOS 17+, od konfiguracji projektu po zaawansowane wzorce z animacjami i zarządzaniem stanem.
Architektura Interaktywnych Widgetów
Interaktywność widgetów iOS 17+ działa poprzez framework App Intents. W przeciwieństwie do tradycyjnych deep linków, które otwierałyby aplikację, App Intents pozwalają wykonywać kod bezpośrednio z widgetu, a następnie automatycznie odświeżać wyświetlanie z nowymi danymi.
import WidgetKit
import SwiftUI
import AppIntents
// Architektura opiera się na trzech głównych komponentach:
// 1. Widget Timeline Provider - dostarcza dane
// 2. Widget View - wyświetla interfejs z Button/Toggle
// 3. App Intent - wykonuje akcję po dotknięciu
struct TaskWidget: Widget {
// Unikalny identyfikator widgetu
let kind: String = "TaskWidget"
var body: some WidgetConfiguration {
// StaticConfiguration dla widgetów bez parametrów
StaticConfiguration(
kind: kind,
provider: TaskTimelineProvider()
) { entry in
TaskWidgetView(entry: entry)
// Wymagane dla App Intents
.containerBackground(.fill.tertiary, for: .widget)
}
.configurationDisplayName("Zadania")
.description("Zarządzaj swoimi zadaniami z ekranu głównego.")
.supportedFamilies([.systemSmall, .systemMedium])
}
}Widget deklaruje swoją konfigurację i określa providera, który będzie dostarczać dane. Atrybut .containerBackground jest obowiązkowy od iOS 17 dla interaktywnych widgetów.
Tworzenie Timeline Provider
Timeline Provider określa, kiedy i jak widget jest odświeżany. Dla interaktywnych widgetów musi również reagować na zmiany powodowane przez App Intents.
import WidgetKit
import SwiftUI
// Entry reprezentujący stan widgetu w danej chwili
struct TaskEntry: TimelineEntry {
let date: Date
let tasks: [Task]
// Stan ładowania dla wizualnej informacji zwrotnej
var isLoading: Bool = false
}
// Model danych współdzielony między aplikacją a widgetem
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 wyświetlany podczas początkowego ładowania
func placeholder(in context: Context) -> TaskEntry {
TaskEntry(
date: Date(),
tasks: [
Task(id: UUID(), title: "Przykładowe zadanie", isCompleted: false, priority: .medium)
]
)
}
// Snapshot dla galerii widgetów
func getSnapshot(in context: Context, completion: @escaping (TaskEntry) -> Void) {
let entry = TaskEntry(
date: Date(),
tasks: TaskDataManager.shared.fetchTasks().prefix(3).map { $0 }
)
completion(entry)
}
// Pełna timeline z polityką odświeżania
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)))
// Odświeżenie za 15 minut lub po akcji użytkownika
let nextUpdate = Calendar.current.date(
byAdding: .minute,
value: 15,
to: Date()
) ?? Date()
let timeline = Timeline(
entries: [entry],
policy: .after(nextUpdate)
)
completion(timeline)
}
}Provider używa współdzielonego TaskDataManager do dostępu do danych. To podejście gwarantuje synchronizację między główną aplikacją a widgetem.
Aby współdzielić dane między aplikacją a widgetem, trzeba skonfigurować App Group w capabilities projektu. UserDefaults lub pliki muszą używać tej współdzielonej grupy.
Menedżer Współdzielonych Danych
Współdzielenie danych między aplikacją a widgetem wymaga wspólnego kontenera dostępnego przez App Group.
import Foundation
final class TaskDataManager {
// Singleton dla globalnego dostępu
static let shared = TaskDataManager()
// Identyfikator App Group skonfigurowany w Xcode
private let appGroupID = "group.com.example.taskapp"
// UserDefaults współdzielony między aplikacją a widgetem
private var sharedDefaults: UserDefaults? {
UserDefaults(suiteName: appGroupID)
}
private let tasksKey = "tasks"
private init() {}
// Pobiera zadania ze współdzielonego magazynu
func fetchTasks() -> [Task] {
guard let data = sharedDefaults?.data(forKey: tasksKey),
let tasks = try? JSONDecoder().decode([Task].self, from: data) else {
return []
}
return tasks
}
// Zapisuje z powiadomieniem widgetu
func saveTasks(_ tasks: [Task]) {
guard let data = try? JSONEncoder().encode(tasks) else { return }
sharedDefaults?.set(data, forKey: tasksKey)
}
// Aktualizuje konkretne zadanie
func updateTask(_ task: Task) {
var tasks = fetchTasks()
if let index = tasks.firstIndex(where: { $0.id == task.id }) {
tasks[index] = task
saveTasks(tasks)
}
}
// Przełącza stan ukończenia
func toggleTaskCompletion(taskID: UUID) {
var tasks = fetchTasks()
if let index = tasks.firstIndex(where: { $0.id == taskID }) {
tasks[index].isCompleted.toggle()
saveTasks(tasks)
}
}
}Menedżer ten enkapsuluje całą logikę persystencji i będzie używany zarówno przez aplikację, jak i App Intents widgetu.
Tworzenie App Intent dla Interaktywności
App Intent definiuje akcję wykonywaną gdy użytkownik wchodzi w interakcję z widgetem. iOS wykonuje tę akcję w tle, a następnie automatycznie odświeża widget.
import AppIntents
import WidgetKit
// Intent do przełączania stanu zadania
struct ToggleTaskIntent: AppIntent {
// Tytuł wyświetlany w skrótach Siri
static var title: LocalizedStringResource = "Przełącz stan zadania"
// Opis dla dostępności
static var description = IntentDescription("Oznacza zadanie jako ukończone lub nieukończone.")
// Parametr: ID zadania do modyfikacji
@Parameter(title: "ID zadania")
var taskID: String
// Wymagany inicjalizator dla AppIntent
init() {}
// Inicjalizator z parametrem do tworzenia z widoku
init(taskID: UUID) {
self.taskID = taskID.uuidString
}
// Wykonanie akcji
func perform() async throws -> some IntentResult {
// Konwersja ID string na UUID
guard let uuid = UUID(uuidString: taskID) else {
return .result()
}
// Aktualizacja zadania
TaskDataManager.shared.toggleTaskCompletion(taskID: uuid)
// Żąda odświeżenia widgetu
WidgetCenter.shared.reloadTimelines(ofKind: "TaskWidget")
return .result()
}
}Wywołanie WidgetCenter.shared.reloadTimelines powoduje natychmiastowe odświeżenie widgetu po akcji, gwarantując natychmiastową wizualną informację zwrotną.
Gotowy na rozmowy o iOS?
Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.
Widok Widgetu z Interaktywnymi Przyciskami
Widok widgetu używa standardowego komponentu SwiftUI Button z intentem jako akcją. iOS 17+ automatycznie przechwytuje te interakcje, aby wykonać App Intent.
import SwiftUI
import WidgetKit
struct TaskWidgetView: View {
let entry: TaskEntry
// Adaptacja do rozmiaru widgetu
@Environment(\.widgetFamily) var family
var body: some View {
VStack(alignment: .leading, spacing: 8) {
// Nagłówek z tytułem i licznikiem
headerView
// Lista zadań z interaktywnymi przyciskami
ForEach(entry.tasks.prefix(tasksLimit)) { task in
TaskRowView(task: task)
}
Spacer(minLength: 0)
}
.padding()
}
// Liczba zadań w zależności od rozmiaru
private var tasksLimit: Int {
switch family {
case .systemSmall: return 2
case .systemMedium: return 3
default: return 4
}
}
private var headerView: some View {
HStack {
Text("Zadania")
.font(.headline)
.fontWeight(.bold)
Spacer()
// Plakietka z liczbą pozostałych zadań
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 {
// Przycisk z App Intent jako akcją
Button(intent: ToggleTaskIntent(taskID: task.id)) {
HStack(spacing: 12) {
// Wskaźnik ukończenia
Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
.font(.title3)
.foregroundStyle(task.isCompleted ? .green : .secondary)
// Tytuł zadania
Text(task.title)
.font(.subheadline)
.strikethrough(task.isCompleted)
.foregroundStyle(task.isCompleted ? .secondary : .primary)
.lineLimit(1)
Spacer()
// Wskaźnik priorytetu
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()
}
}
}Składnia Button(intent:) bezpośrednio łączy przycisk z App Intent. Po dotknięciu iOS wykonuje perform(), a następnie automatycznie odświeża widget.
Interaktywny Toggle dla Widgetów
Dla akcji typu włącz/wyłącz komponent Toggle oferuje alternatywę dla przycisku z natywnym stylem iOS.
import SwiftUI
import AppIntents
// Specyficzny intent dla Toggle z jawnym stanem
struct SetTaskCompletionIntent: AppIntent {
static var title: LocalizedStringResource = "Ustaw stan zadania"
@Parameter(title: "ID zadania")
var taskID: String
// Stan docelowy: true = ukończone, false = nieukończone
@Parameter(title: "Ukończone")
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 }) {
// Ustawia stan jawnie (nie 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()
// Interaktywny toggle z intentem
Toggle(
isOn: task.isCompleted,
intent: SetTaskCompletionIntent(
taskID: task.id,
isCompleted: !task.isCompleted
)
)
.toggleStyle(.switch)
.labelsHidden()
}
.padding(.vertical, 4)
}
}Toggle zapewnia bardziej intuicyjną interakcję dla stanów binarnych i naturalnie integruje się z designem iOS.
Widgety nie mogą wyświetlać alertów, sheet ani nawigacji. Wszystkie akcje muszą być autonomiczne i bezpośrednio aktualizować widoczny stan.
Animacje Odświeżania i Przejścia
iOS 17+ pozwala animować przejścia podczas odświeżania widgetu po akcji. Modyfikator .contentTransition kontroluje te animacje.
import SwiftUI
import WidgetKit
struct AnimatedTaskRowView: View {
let task: Task
var body: some View {
Button(intent: ToggleTaskIntent(taskID: task.id)) {
HStack(spacing: 12) {
// Ikona z animacją przejścia
Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
.font(.title3)
.foregroundStyle(task.isCompleted ? .green : .secondary)
// Animacja ikony przy zmianie
.contentTransition(.symbolEffect(.replace))
Text(task.title)
.font(.subheadline)
.strikethrough(task.isCompleted)
.foregroundStyle(task.isCompleted ? .secondary : .primary)
// Animacja tekstu
.contentTransition(.opacity)
Spacer()
}
.padding(.vertical, 6)
.padding(.horizontal, 10)
.background(
RoundedRectangle(cornerRadius: 8)
.fill(task.isCompleted ? Color.green.opacity(0.1) : Color.clear)
)
// Animacja tła
.animation(.easeInOut(duration: 0.3), value: task.isCompleted)
}
.buttonStyle(.plain)
}
}
// Widget z animowaną inwalidacją
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("Animowane Zadania")
.description("Widgety z płynnymi animacjami.")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
// Aktywacja animacji zawartości
.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("Zadania")
.font(.headline.bold())
Spacer()
let completed = entry.tasks.filter(\.isCompleted).count
let total = entry.tasks.count
// Animowany postęp
Text("\(completed)/\(total)")
.font(.caption.bold())
.foregroundStyle(.secondary)
.contentTransition(.numericText())
}
}
}Animacje .symbolEffect(.replace) i .numericText() tworzą płynne przejścia między stanami, znacząco poprawiając doświadczenie użytkownika.
Konfigurowalny Widget z AppIntentConfiguration
Dla widgetów konfigurowalnych przez użytkownika (filtry, kategorie) AppIntentConfiguration zastępuje StaticConfiguration.
import WidgetKit
import SwiftUI
import AppIntents
// Konfiguracja udostępniona użytkownikowi
struct TaskWidgetConfigurationIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource = "Konfiguracja zadań"
static var description = IntentDescription("Dostosuj wyświetlanie zadań.")
// Filtr według priorytetu
@Parameter(title: "Priorytet", default: .all)
var priorityFilter: PriorityFilter
// Pokaż ukończone zadania
@Parameter(title: "Pokaż ukończone", default: true)
var showCompleted: Bool
// Maksymalna liczba zadań
@Parameter(title: "Liczba zadań", default: 3)
var maxTasks: Int
}
// Enum dla filtra priorytetu
enum PriorityFilter: String, AppEnum {
case all
case high
case medium
case low
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Priorytet"
static var caseDisplayRepresentations: [PriorityFilter: DisplayRepresentation] = [
.all: "Wszystkie",
.high: "Wysoki",
.medium: "Średni",
.low: "Niski"
]
}
// Provider dostosowany do konfiguracji
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))
}
// Stosuje filtry konfiguracyjne
private func filteredTasks(for config: TaskWidgetConfigurationIntent) -> [Task] {
var tasks = TaskDataManager.shared.fetchTasks()
// Filtrowanie według priorytetu
if config.priorityFilter != .all {
let priority = Task.Priority(rawValue: config.priorityFilter.rawValue) ?? .medium
tasks = tasks.filter { $0.priority == priority }
}
// Filtrowanie ukończonych w razie potrzeby
if !config.showCompleted {
tasks = tasks.filter { !$0.isCompleted }
}
// Ograniczenie liczby
return Array(tasks.prefix(config.maxTasks))
}
}
// Widget z konfiguracją użytkownika
struct ConfigurableTaskWidget: Widget {
let kind: String = "ConfigurableTaskWidget"
var body: some WidgetConfiguration {
// AppIntentConfiguration dla konfigurowalnych widgetów
AppIntentConfiguration(
kind: kind,
intent: TaskWidgetConfigurationIntent.self,
provider: ConfigurableTaskProvider()
) { entry in
TaskWidgetView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
}
.configurationDisplayName("Spersonalizowane Zadania")
.description("Filtruj i personalizuj swoje zadania.")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
}
}Użytkownik może teraz konfigurować widget poprzez długie naciśnięcie, otrzymując spersonalizowane doświadczenie bez dodatkowego kodu w aplikacji.
Obsługa Błędów i Stanów Ładowania
Dobre UX wymaga obsługi przypadków błędów i stanów pośrednich podczas interakcji.
import AppIntents
import WidgetKit
struct ToggleTaskWithFeedbackIntent: AppIntent {
static var title: LocalizedStringResource = "Przełącz zadanie z feedbackiem"
@Parameter(title: "ID zadania")
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 {
// Zwraca cichy błąd
return .result(value: false)
}
// Symuluje operację asynchroniczną (np. sync z serwerem)
do {
try await Task.sleep(for: .milliseconds(100))
TaskDataManager.shared.toggleTaskCompletion(taskID: uuid)
WidgetCenter.shared.reloadTimelines(ofKind: "TaskWidget")
return .result(value: true)
} catch {
// Błąd: nie aktualizuj widgetu
return .result(value: false)
}
}
}
// Widok ze stanem ładowania
struct TaskRowWithLoadingView: View {
let task: Task
@State private var isLoading = false
var body: some View {
Button(intent: ToggleTaskIntent(taskID: task.id)) {
HStack(spacing: 12) {
// Wskaźnik warunkowy
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)
}
}Natychmiastowa wizualna informacja zwrotna (zmniejszona przezroczystość, wskaźnik ładowania) informuje użytkownika, że jego akcja została zarejestrowana.
Najlepsze Praktyki i Optymalizacje
Kilka wzorców gwarantuje wydajne i niezawodne interaktywne widgety.
import WidgetKit
import SwiftUI
// 1. Zawsze unieważniaj cache po modyfikacji
final class WidgetRefreshManager {
static func refreshAllWidgets() {
// Odświeżenie wszystkich widgetów aplikacji
WidgetCenter.shared.reloadAllTimelines()
}
static func refreshWidget(kind: String) {
// Odświeżenie konkretnego widgetu
WidgetCenter.shared.reloadTimelines(ofKind: kind)
}
// Wywołanie z aplikacji po modyfikacji danych
static func notifyDataChanged() {
Task { @MainActor in
refreshAllWidgets()
}
}
}
// 2. Ogranicz złożoność widoków
struct OptimizedWidgetView: View {
let entry: TaskEntry
var body: some View {
// Preferuj proste widoki bez GeometryReader
VStack(alignment: .leading, spacing: 8) {
ForEach(entry.tasks.prefix(3)) { task in
// Lekkie komponenty
minimalTaskRow(task)
}
}
.padding()
}
// Minimalny widok dla wydajności
@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. Używaj @AppStorage dla prostych stanów
struct QuickSettingsWidgetView: View {
// Bezpośredni dostęp do współdzielonych UserDefaults
@AppStorage("showCompletedTasks", store: UserDefaults(suiteName: "group.com.example.app"))
private var showCompleted = true
var body: some View {
// Stan utrzymuje się między odświeżeniami
Text(showCompleted ? "Pokazuję wszystkie" : "Ukrywam ukończone")
}
}
// 4. Wstępnie obliczaj dane w providerze
struct OptimizedTaskEntry: TimelineEntry {
let date: Date
let tasks: [Task]
// Wstępnie obliczone dane
let completedCount: Int
let pendingCount: Int
let highPriorityCount: Int
init(date: Date, tasks: [Task]) {
self.date = date
self.tasks = tasks
// Obliczenia wykonywane raz
self.completedCount = tasks.filter(\.isCompleted).count
self.pendingCount = tasks.filter { !$0.isCompleted }.count
self.highPriorityCount = tasks.filter { $0.priority == .high && !$0.isCompleted }.count
}
}Te optymalizacje gwarantują responsywne widgety, które nie zużywają nadmiernie baterii.
Używaj schematu widget w Xcode do debugowania. Podgląd canvas pozwala testować różne rozmiary i stany bez instalacji na urządzeniu.
Podsumowanie
WidgetKit iOS 17+ z App Intents przekształca widgety w prawdziwe interaktywne rozszerzenia aplikacji iOS. Ta deklaratywna architektura znacząco upraszcza rozwój, oferując natywne i płynne doświadczenie użytkownika.
Lista Kontrolna Interaktywnego Widgetu iOS 17+
- ✅ Skonfiguruj App Group dla współdzielenia danych
- ✅ Stwórz Timeline Provider z odpowiednim odświeżaniem
- ✅ Zaimplementuj App Intents dla każdej akcji
- ✅ Używaj
Button(intent:)lubToggle(intent:)dla interaktywności - ✅ Wywołuj
WidgetCenter.shared.reloadTimelinespo modyfikacji - ✅ Dodaj obowiązkowy
.containerBackgrounddla iOS 17+ - ✅ Zaimplementuj płynne animacje przejścia
- ✅ Obsłuż stany ładowania i błędów
- ✅ Optymalizuj widoki pod kątem wydajności baterii
- ✅ Testuj na wszystkich obsługiwanych rozmiarach widgetu
Zacznij ćwiczyć!
Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.
Tagi
Udostępnij
Powiązane artykuły

App Intents i Siri Shortcuts: zaawansowana automatyzacja iOS 2026
Kompletny przewodnik po App Intents i Siri Shortcuts dla iOS 18+. Tworzenie własnych akcji Siri, integracja Apple Intelligence i automatyzacja aplikacji Swift w 2026.

Combine vs async/await w Swift: Wzorce Progresywnej Migracji
Kompletny przewodnik po migracji z Combine do async/await w Swift: progresywne strategie, wzorce mostkowania i współistnienie paradygmatów w bazach kodu iOS.

Pytania rekrutacyjne o dostępność iOS w 2026: VoiceOver i Dynamic Type
Przygotuj się do rozmów iOS z kluczowymi pytaniami o dostępność: VoiceOver, Dynamic Type, semantyczne traits i audyty.