# SwiftUI: створення сучасних інтерфейсів для iOS > Посібник зі створення сучасних інтерфейсів за допомогою SwiftUI: декларативний синтаксис, компоненти, анімації та найкращі практики для iOS 18. - Published: 2026-01-05 - Updated: 2026-04-02 - Author: SharpSkill - Tags: swiftui, ios, swift, ui, apple - Reading time: 12 min --- SwiftUI докорінно змінив підхід до розробки інтерфейсів на платформах Apple. Завдяки декларативному синтаксису та нативній інтеграції цей фреймворк дозволяє створювати елегантні застосунки з меншою кількістю коду, ніж будь-коли раніше. iOS 18 приносить суттєві покращення продуктивності та нові можливості. > **Why SwiftUI in 2026?** > > SwiftUI досяг зрілості в iOS 18, пропонуючи оптимізований рендеринг, покращене управління пам'яттю та спрощену інтеграцію з UIKit. Це стандарт для нових iOS-застосунків. ## Розуміння декларативної парадигми Перед тим як писати код, варто розібратися, чим SwiftUI відрізняється від попередників. У UIKit (старішому фреймворку) розробник мав покроково описувати iOS **як саме** будувати інтерфейс: "створи мітку, розмісти її тут, зміни колір при натисканні". Це **імперативний** підхід. SwiftUI працює інакше: розробник описує **що** потрібно відобразити, а фреймворк бере на себе решту. Різницю можна порівняти з GPS-навігацією: імперативний підхід — це покрокові вказівки повороту, а декларативний — просто вказівка кінцевого пункту призначення. ### Перший View У SwiftUI кожен елемент інтерфейсу — це `View`. View є структурою (struct), що описує вміст екрану. Ось перший екран із заголовком, підзаголовком та кнопкою: ```swift // ContentView.swift import SwiftUI // Each screen is a struct that implements the View protocol struct ContentView: View { // The "body" property describes what the view displays // "some View" means "a type of View, but Swift figures it out automatically" var body: some View { // VStack = "Vertical Stack": stacks elements vertically // spacing: 20 = 20 points of space between each element VStack(spacing: 20) { // A simple text with style modifiers Text("Welcome to SharpSkill") .font(.largeTitle) // Large title font .fontWeight(.bold) // Bold text Text("Prepare for your iOS interviews") .font(.subheadline) // Smaller font .foregroundColor(.secondary) // Gray secondary color // Button with an action (closure) and a label Button("Get Started") { print("Button tapped!") } .buttonStyle(.borderedProminent) // Filled blue button style } .padding() // Adds margins around the VStack } } ``` Варто звернути увагу на структуру: спочатку оголошується те, що потрібно (тексти, кнопка), потім елементи розміщуються вертикально (VStack), а стилі застосовуються через **модифікатори** (`.font()`, `.padding()`). > **Ключовий висновок**: у SwiftUI інтерфейсні об'єкти не "створюються" вручну — натомість **описується** бажаний інтерфейс. SwiftUI самостійно керує створенням, оновленням та видаленням фактичних елементів. ### Модифікатори: трансформація View Модифікатори — це методи, що послідовно викликаються після View для його перетворення. Їх можна порівняти з фільтрами, що накладаються один за одним. **Порядок має значення**, оскільки кожен модифікатор створює новий View, що обгортає попередній. Приклад, що демонструє важливість порядку: ```swift // ModifiersOrder.swift // Example 1: padding THEN background Text("SwiftUI") .padding() // 1. Adds 16pt of space around the text .background(.blue) // 2. Blue background covers text + padding .foregroundColor(.white) // Result: white text on blue rectangle with margins // Example 2: background THEN padding (reversed order!) Text("SwiftUI") .background(.blue) // 1. Tight blue background around text only .padding() // 2. Transparent padding around blue background .foregroundColor(.white) // Result: white text on small blue rectangle, surrounded by empty space ``` Різниця полягає ось у чому: в першому випадку відступ знаходиться "всередині" фону, а в другому — "зовні". Деталь тонка, проте критично важлива для побудови макетів. ## Організація інтерфейсів за допомогою Stacks SwiftUI пропонує три основні контейнери для організації View. Їх можна уявити як коробки, що по-різному розташовують свій вміст. ### VStack, HStack та ZStack - **VStack** (Vertical Stack): розміщує елементи зверху вниз - **HStack** (Horizontal Stack): вирівнює елементи зліва направо - **ZStack** (Z-axis Stack): накладає елементи один на одного Створимо картку профілю користувача, що поєднує всі три стеки. Мета: відобразити фото з позначкою верифікації, ім'я та роль користувача. ```swift // ProfileCard.swift struct ProfileCard: View { var body: some View { // Main HStack: photo on left, info on right HStack(spacing: 16) { // ZStack to overlay the badge on the photo ZStack(alignment: .bottomTrailing) { // Profile image (circle) Image("avatar") .resizable() .frame(width: 60, height: 60) .clipShape(Circle()) // Green "verified" badge at bottom right Image(systemName: "checkmark.circle.fill") .foregroundColor(.green) .background(Circle().fill(.white)) // White circle behind } // VStack to stack name and role vertically VStack(alignment: .leading, spacing: 4) { Text("Marie Dupont") .font(.headline) Text("iOS Developer") .font(.subheadline) .foregroundColor(.secondary) } // Spacer pushes everything to the left Spacer() // Chevron on right indicates it's tappable Image(systemName: "chevron.right") .foregroundColor(.gray) } .padding() .background(Color(.systemGray6)) .cornerRadius(12) } } ``` Ключовий прийом — `Spacer()`: це невидимий елемент, що займає весь доступний простір. Без нього елементи були б відцентровані. З ним — контент притискається ліворуч, а шеврон залишається праворуч. > **Xcode Tip** > > Комбінація `⌘ + click` на будь-якому View в Xcode відкриває візуальний інспектор. Модифікатори можна додавати без ручного введення коду. ## Управління станом: @State та @Binding Управління станом — серце SwiftUI. Стан — це будь-які дані, що можуть змінюватися та мають оновлювати інтерфейс. Коли стан змінюється, SwiftUI автоматично перераховує відповідні View. ### @State: локальний стан View `@State` — це обгортка властивості (property wrapper), яка повідомляє SwiftUI: "слідкуй за цією змінною та оновлюй View при її зміні". Ідеально підходить для локального стану одного View. Створимо інтерактивний лічильник для розуміння механізму: ```swift // CounterView.swift struct CounterView: View { // @State creates a "source of truth" for this view // private because state shouldn't be modified from outside @State private var count = 0 var body: some View { VStack(spacing: 30) { // This Text updates automatically when count changes Text("\(count)") .font(.system(size: 72, weight: .bold)) HStack(spacing: 40) { // Decrement button Button(action: { count -= 1 // Modifies state → view refreshes }) { Image(systemName: "minus.circle.fill") .font(.largeTitle) } // Increment button Button(action: { count += 1 }) { Image(systemName: "plus.circle.fill") .font(.largeTitle) } } } } } ``` При натисканні кнопки значення `count` змінюється. SwiftUI виявляє цю зміну та повторно виконує `body` для оновлення відображення. Мітку не потрібно оновлювати вручну — все відбувається автоматично. ### @Binding: спільний стан між View Іноді дочірньому View потрібно змінювати стан батьківського View. Для цього існує `@Binding` — двосторонній зв'язок з наявним `@State`. Розглянемо конкретний приклад: поле введення імені користувача з валідацією в реальному часі. ```swift // UsernameValidation.swift // Parent view: owns the state struct SignupForm: View { @State private var username = "" // Source of truth @State private var isValid = false // Validation state var body: some View { VStack(spacing: 20) { // We pass BINDINGS (with $) to the child view UsernameField(username: $username, isValid: $isValid) Button("Create Account") { // Submit the form } .disabled(!isValid) // Disabled if invalid .buttonStyle(.borderedProminent) } .padding() } } ``` Дочірній View отримує біндінги та може їх модифікувати: ```swift // Child view: receives and modifies state via @Binding struct UsernameField: View { @Binding var username: String // Connection to parent's @State @Binding var isValid: Bool var body: some View { VStack(alignment: .leading, spacing: 8) { TextField("Username", text: $username) .textFieldStyle(.roundedBorder) .onChange(of: username) { oldValue, newValue in // Validation: at least 3 characters isValid = newValue.count >= 3 } // Visual feedback HStack { Image(systemName: isValid ? "checkmark.circle" : "xmark.circle") Text("Minimum 3 characters") } .font(.caption) .foregroundColor(isValid ? .green : .red) } } } ``` Коли користувач вводить текст у TextField, `username` змінюється через біндінг. Батьківський View бачить цю зміну та може її використовувати. Це чистий двосторонній зв'язок. > **Warning** > > Використовувати `@State` слід лише для простого, локального стану. Для даних, що поділяються між кількома екранами, або складної логіки рекомендовано `@Observable` (iOS 17+) чи патерни MVVM. ## Відображення динамічних списків Списки — невід'ємна частина мобільних застосунків. SwiftUI надає `List` для відображення колекцій даних із нативним стилем iOS (роздільники, дії при свайпі тощо). ### Створення простого списку Для відображення списку потрібні дві речі: дані та спосіб їхньої ідентифікації. Протокол `Identifiable` дозволяє SwiftUI визначити відповідність між View та елементами даних. Спочатку визначимо модель даних: ```swift // Models/Interview.swift // Identifiable lets SwiftUI track each element struct Interview: Identifiable { let id = UUID() // Auto-generated unique identifier let technology: String let difficulty: String let questionCount: Int } ``` Тепер створимо список. Ідея полягає в ітерації по даних за допомогою `ForEach` та створенні рядка для кожного елемента: ```swift // InterviewListView.swift struct InterviewListView: View { // Data to display (in reality, this would come from an API) @State private var interviews = [ Interview(technology: "iOS", difficulty: "Intermediate", questionCount: 25), Interview(technology: "Android", difficulty: "Advanced", questionCount: 30), Interview(technology: "React", difficulty: "Beginner", questionCount: 20) ] var body: some View { // NavigationStack enables the navigation bar NavigationStack { List { // ForEach iterates over each interview // Thanks to Identifiable, no need to specify id: ForEach(interviews) { interview in InterviewRow(interview: interview) } } .navigationTitle("My Interviews") } } } ``` А ось View для кожного рядка, винесений в окремий компонент для зручності читання: ```swift // InterviewRow.swift struct InterviewRow: View { let interview: Interview var body: some View { HStack { // Left column: title and subtitle VStack(alignment: .leading, spacing: 4) { Text(interview.technology) .font(.headline) Text(interview.difficulty) .font(.subheadline) .foregroundColor(.secondary) } Spacer() // Badge with question count Text("\(interview.questionCount) Q") .font(.caption) .fontWeight(.medium) .padding(.horizontal, 8) .padding(.vertical, 4) .background(Color.blue.opacity(0.1)) .cornerRadius(8) } .padding(.vertical, 4) } } ``` Винесення `InterviewRow` в окрему структуру робить код читабельнішим та придатним для повторного використання. Це одна з найкращих практик SwiftUI. ### Додавання дій: видалення та переміщення Списки iOS нативно підтримують видалення (свайп ліворуч) та переміщення (перетягування). SwiftUI робить це тривіальним завдяки `.onDelete` та `.onMove`: ```swift // InterviewListView.swift (enhanced version) struct InterviewListView: View { @State private var interviews = [/* ... data ... */] var body: some View { NavigationStack { List { ForEach(interviews) { interview in InterviewRow(interview: interview) } // Swipe to delete .onDelete(perform: deleteInterview) // Drag and drop to reorder .onMove(perform: moveInterview) } .navigationTitle("My Interviews") .toolbar { // "Edit" button that activates edit mode EditButton() } } } // Deletes elements at specified indices private func deleteInterview(at offsets: IndexSet) { interviews.remove(atOffsets: offsets) } // Moves elements from one position to another private func moveInterview(from source: IndexSet, to destination: Int) { interviews.move(fromOffsets: source, toOffset: destination) } } ``` Лише 4 додаткові рядки коду (`.onDelete`, `.onMove`, `EditButton` та дві функції) — і список стає повністю інтерактивним. Саме в цьому полягає сила SwiftUI. ## Анімація інтерфейсів SwiftUI чудово працює з анімаціями. На відміну від UIKit, де анімації вимагали значного обсягу коду, тут все декларативне: описується кінцевий стан, а SwiftUI анімує перехід. ### Неявні анімації з withAnimation Найпростіший спосіб анімації — обгорнути зміну стану в `withAnimation`. SwiftUI визначає, що змінилося, та автоматично анімує відповідні візуальні властивості. ```swift // AnimatedCard.swift struct AnimatedCard: View { @State private var isExpanded = false var body: some View { VStack { RoundedRectangle(cornerRadius: 20) .fill(.blue) // Dimensions change based on state .frame( width: isExpanded ? 300 : 150, height: isExpanded ? 200 : 100 ) Button(isExpanded ? "Collapse" : "Expand") { // withAnimation animates ALL visual changes // resulting from this state change withAnimation(.spring(response: 0.5, dampingFraction: 0.7)) { isExpanded.toggle() } } .padding(.top) } } } ``` При натисканні кнопки `isExpanded` перемикається. Завдяки `withAnimation` зміна розміру прямокутника анімується з ефектом пружини. Не потрібно вказувати, що саме анімувати — SwiftUI визначить це самостійно. ### Переходи: анімація появи та зникнення Переходи (transitions) визначають, як View з'являється або зникає. За замовчуванням це затухання (`opacity`), але поведінку можна налаштувати: ```swift // TransitionDemo.swift struct TransitionDemo: View { @State private var showDetails = false var body: some View { VStack(spacing: 20) { Button("Show Details") { withAnimation(.easeInOut(duration: 0.3)) { showDetails.toggle() } } // This view appears/disappears with a transition if showDetails { DetailCard() // Asymmetric transition: different for entry and exit .transition( .asymmetric( insertion: .scale.combined(with: .opacity), // Entry: zoom + fade removal: .slide // Exit: slide ) ) } } } } struct DetailCard: View { var body: some View { VStack(alignment: .leading, spacing: 12) { Text("Interview Details") .font(.headline) Text("25 questions • 45 minutes") .foregroundColor(.secondary) } .padding() .background(Color(.systemGray6)) .cornerRadius(16) } } ``` Тут картка з'являється з ефектом масштабування з центру (scale + opacity) та зникає зісковзуючи вбік (slide). Такі мікроанімації роблять інтерфейс живим та професійним. > **Performance** > > SwiftUI автоматично оптимізує анімації. Для природного відчуття рекомендовано `.spring()`, а надмірно довгих анімацій (> 0.5с) варто уникати, щоб не дратувати користувачів. ## Завантаження асинхронних даних У реальних застосунках дані зазвичай надходять з API. Swift Concurrency (async/await) ідеально інтегрується зі SwiftUI через модифікатор `.task`. ### Патерн: завантаження / помилка / успіх Ось стандартний патерн для відображення даних з API. Обробляються три стани: завантаження, помилка та успіх. ```swift // AsyncDataView.swift struct AsyncDataView: View { @State private var questions: [Question] = [] @State private var isLoading = true @State private var errorMessage: String? var body: some View { Group { if isLoading { // State: loading in progress ProgressView("Loading...") } else if let error = errorMessage { // State: error VStack(spacing: 16) { Image(systemName: "exclamationmark.triangle") .font(.largeTitle) .foregroundColor(.orange) Text(error) Button("Retry") { Task { await loadQuestions() } } } } else { // State: success, display data List(questions) { question in Text(question.title) } } } // .task runs automatically when the view appears .task { await loadQuestions() } // Pull-to-refresh .refreshable { await loadQuestions() } } private func loadQuestions() async { isLoading = true errorMessage = nil do { // Async API call questions = try await QuestionService.shared.fetchQuestions() } catch { errorMessage = "Unable to load questions" } isLoading = false } } ``` Модифікатор `.task` є ключовим: він запускає асинхронне завдання при появі View та автоматично скасовує його при зникненні. Витоки пам'яті виключені. ## Підсумки SwiftUI став невід'ємною частиною сучасної розробки для iOS. З виходом iOS 18 фреймворк досяг рівня зрілості, що робить його повністю придатним для продакшн-застосунків. ### Ключові висновки - **Декларативна парадигма**: описується бажаний результат, а не спосіб його досягнення - **@State та @Binding**: реактивне управління станом та його передача між View - **Stacks**: комбінація VStack, HStack та ZStack для гнучких макетів - **List**: відображення колекцій з мінімальним кодом та нативними взаємодіями - **Анімації**: використання `withAnimation` для автоматичних плавних переходів - **Async/await**: завантаження даних через `.task` з обробкою станів завантаження та помилок ### Контрольний список - Розуміння різниці між імперативним (UIKit) та декларативним (SwiftUI) підходами - Освоєння модифікаторів та порядку їхнього застосування - Знання, коли використовувати @State, @Binding та @Observable - Побудова макетів за допомогою Stacks - Реалізація списків з діями (видалення, переміщення) - Анімація зміни стану за допомогою withAnimation SwiftUI відкриває можливості для створення елегантних застосунків у всій екосистемі Apple. Найкращий спосіб навчитися — практика: варто створити невеликий персональний проект та поекспериментувати з кожною концепцією з цієї статті. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/uk/blog/ios/swiftui-building-modern-interfaces