SwiftUI: створення сучасних інтерфейсів для iOS

Посібник зі створення сучасних інтерфейсів за допомогою SwiftUI: декларативний синтаксис, компоненти, анімації та найкращі практики для iOS 18.

Посібник зі SwiftUI для створення сучасних інтерфейсів iOS

SwiftUI докорінно змінив підхід до розробки інтерфейсів на платформах Apple. Завдяки декларативному синтаксису та нативній інтеграції цей фреймворк дозволяє створювати елегантні застосунки з меншою кількістю коду, ніж будь-коли раніше. iOS 18 приносить суттєві покращення продуктивності та нові можливості.

Why SwiftUI in 2026?

SwiftUI досяг зрілості в iOS 18, пропонуючи оптимізований рендеринг, покращене управління пам'яттю та спрощену інтеграцію з UIKit. Це стандарт для нових iOS-застосунків.

Розуміння декларативної парадигми

Перед тим як писати код, варто розібратися, чим SwiftUI відрізняється від попередників. У UIKit (старішому фреймворку) розробник мав покроково описувати iOS як саме будувати інтерфейс: "створи мітку, розмісти її тут, зміни колір при натисканні". Це імперативний підхід.

SwiftUI працює інакше: розробник описує що потрібно відобразити, а фреймворк бере на себе решту. Різницю можна порівняти з GPS-навігацією: імперативний підхід — це покрокові вказівки повороту, а декларативний — просто вказівка кінцевого пункту призначення.

Перший View

У SwiftUI кожен елемент інтерфейсу — це View. View є структурою (struct), що описує вміст екрану. Ось перший екран із заголовком, підзаголовком та кнопкою:

ContentView.swiftswift
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, що обгортає попередній.

Приклад, що демонструє важливість порядку:

ModifiersOrder.swiftswift
// 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): накладає елементи один на одного

Створимо картку профілю користувача, що поєднує всі три стеки. Мета: відобразити фото з позначкою верифікації, ім'я та роль користувача.

ProfileCard.swiftswift
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.

Створимо інтерактивний лічильник для розуміння механізму:

CounterView.swiftswift
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.

Розглянемо конкретний приклад: поле введення імені користувача з валідацією в реальному часі.

UsernameValidation.swiftswift
// 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.

Готовий до співбесід з iOS?

Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.

Відображення динамічних списків

Списки — невід'ємна частина мобільних застосунків. SwiftUI надає List для відображення колекцій даних із нативним стилем iOS (роздільники, дії при свайпі тощо).

Створення простого списку

Для відображення списку потрібні дві речі: дані та спосіб їхньої ідентифікації. Протокол Identifiable дозволяє SwiftUI визначити відповідність між View та елементами даних.

Спочатку визначимо модель даних:

Models/Interview.swiftswift
// 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 та створенні рядка для кожного елемента:

InterviewListView.swiftswift
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 для кожного рядка, винесений в окремий компонент для зручності читання:

InterviewRow.swiftswift
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:

InterviewListView.swift (enhanced version)swift
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 визначає, що змінилося, та автоматично анімує відповідні візуальні властивості.

AnimatedCard.swiftswift
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), але поведінку можна налаштувати:

TransitionDemo.swiftswift
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. Обробляються три стани: завантаження, помилка та успіх.

AsyncDataView.swiftswift
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. Найкращий спосіб навчитися — практика: варто створити невеликий персональний проект та поекспериментувати з кожною концепцією з цієї статті.

Теги

#swiftui
#ios
#swift
#ui
#apple

Поділитися

Пов'язані статті