SwiftUI: Tworzenie Nowoczesnych Interfejsow dla iOS
Kompletny przewodnik po tworzeniu nowoczesnych interfejsow uzytkownika w SwiftUI: skladnia deklaratywna, komponenty, animacje i najlepsze praktyki dla iOS 18.

SwiftUI zmienil sposob tworzenia interfejsow na wszystkich platformach Apple. Dzieki deklaratywnej skladni i natywnej integracji framework ten pozwala budowac eleganckie aplikacje przy znacznie mniejszej ilosci kodu niz kiedykolwiek wczesniej. iOS 18 przynosi istotne usprawnienia w zakresie wydajnosci oraz nowe mozliwosci.
SwiftUI osiagnal pelna dojrzalosc wraz z iOS 18, oferujac zoptymalizowane renderowanie, lepsze zarzadzanie pamiecia i uproszczona integracje z UIKit. Jest standardem dla nowych aplikacji iOS.
Zrozumienie Paradygmatu Deklaratywnego
Zanim zostanie napisana pierwsza linia kodu, warto zrozumiec, co wyruznia SwiftUI. W UIKit (starszym frameworku) programista musial instruowac iOS krok po kroku, jak zbudowac interfejs: "utworz etykiete, umiesci ja tutaj, zmien kolor gdy uzytkownik kliknie". To paradygmat imperatywny.
SwiftUI dziala inaczej: programista opisuje co chce wyswietlic, a framework zajmuje sie reszta. Roznica jest podobna do tej miedzy podawaniem wskazowek zakret po zakrecie (imperatywnie) a prostym podaniem celu podrozy (deklaratywnie).
Pierwsza View
W SwiftUI kazdy element interfejsu jest View. View to struktura (struct) opisujaca, co powinno pojawic sie na ekranie. Ponizej pierwszy ekran z tytulem, podtytulem i przyciskiem:
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
}
}Warto zwrocic uwage na strukture: deklarowane sa porzadane elementy (teksty, przycisk), ukladane sa pionowo (VStack), a style aplikowane sa za pomoca modyfikatorow (.font(), .padding()).
Kluczowa zasada: W SwiftUI nie "tworzy sie" obiektow UI -- opisuje sie porzadany interfejs. SwiftUI samodzielnie zarzadza tworzeniem, aktualizowaniem i niszczeniem rzeczywistych elementow.
Modyfikatory: Przeksztalcanie Widokow
Modyfikatory to metody lancuchowane po widoku w celu jego przeksztalcenia. Dzialaja jak filtry nakladane jeden po drugim. Kolejnosc ma znaczenie, poniewaz kazdy modyfikator tworzy nowy widok opakowujacy poprzedni.
Ponizszy przyklad ilustruje, dlaczego kolejnosc jest kluczowa:
// 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 spaceRoznica? W pierwszym przypadku padding jest "wewnatrz" tla. W drugim -- "na zewnatrz". Subtelna, ale fundamentalna roznica przy opanowywaniu layoutow.
Organizowanie Interfejsow za Pomoca Stackow
SwiftUI oferuje trzy glowne kontenery do organizowania widokow. Mozna je traktowac jak pudelka ukladajace zawartosc na rozne sposoby.
VStack, HStack i ZStack
- VStack (Vertical Stack): uklada elementy od gory do dolu
- HStack (Horizontal Stack): ustawia elementy od lewej do prawej
- ZStack (Z-axis Stack): naklada elementy jeden na drugi
Ponizej budowana jest karta profilu uzytkownika laczaca wszystkie trzy stacki. Cel: wyswietlenie zdjecia ze znaczkiem weryfikacji, a nastepnie imienia i roli uzytkownika.
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)
}
}Kluczowym elementem jest Spacer(): niewidoczny element zajmujacy cala dostepna przestrzen. Bez niego elementy bylyby wycentrowane. Dzieki niemu sa przesuwane w lewo, a chevron pozostaje zakotwiczony po prawej stronie.
Uzycie ⌘ + click na dowolnym widoku w Xcode otwiera wizualny inspektor. Modyfikatory mozna dodawac bez pisania kodu.
Zarzadzanie Stanem: @State i @Binding
Zarzadzanie stanem stanowi serce SwiftUI. Stan to kazde dane, ktore moga sie zmieniac i powinny aktualizowac interfejs. Gdy stan sie zmienia, SwiftUI automatycznie przelicza dotkniety widok.
@State: Lokalny Stan Widoku
@State to property wrapper informujacy SwiftUI: "obserwuj te zmienna i odswiez widok, gdy sie zmieni". Jest idealny do lokalnego stanu pojedynczego widoku.
Ponizej interaktywny licznik ilustrujacy ten mechanizm:
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)
}
}
}
}
}Po tapnieciu przycisku wartosc count sie zmienia. SwiftUI wykrywa te zmiane i ponownie wykonuje body, aby zaktualizowac wyswietlanie. Nie ma potrzeby recznego aktualizowania etykiety -- wszystko odbywa sie automatycznie.
@Binding: Wspoldzielenie Stanu Miedzy Widokami
Czasami widok potomny musi modyfikowac stan widoku nadrzednego. Do tego sluzy @Binding: tworzy dwukierunkowe polaczenie z istniejacym @State.
Ponizej konkretny przyklad: pole wprowadzania nazwy uzytkownika z walidacja w czasie rzeczywistym.
// 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()
}
}Widok potomny otrzymuje bindingi i moze je modyfikowac:
// 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)
}
}
}Gdy uzytkownik wpisuje tekst w TextField, username jest modyfikowany za posrednictwem bindingu. Widok nadrzedny widzi te zmiane i moze z niej korzystac. Jest to przejrzysta komunikacja dwukierunkowa.
@State nalezy stosowac wylacznie do prostych, lokalnych stanow. Dla danych wspoldzielonych miedzy wieloma ekranami lub zlozonej logiki lepiej siegnac po @Observable (iOS 17+) lub wzorce MVVM.
Gotowy na rozmowy o iOS?
Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.
Wyswietlanie Dynamicznych List
Listy sa wszechobecne w aplikacjach mobilnych. SwiftUI udostepnia List do wyswietlania kolekcji danych z natywna stylizacja iOS (separatory, akcje przesuwania itp.).
Tworzenie Prostej Listy
Do wyswietlenia listy potrzebne sa dwie rzeczy: dane i sposob ich identyfikacji. Protokol Identifiable informuje SwiftUI, ktory widok odpowiada ktoremu elementowi danych.
Najpierw definiowany jest model danych:
// 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
}Teraz lista. Idea polega na iterowaniu po danych za pomoca ForEach i tworzeniu wiersza dla kazdego elementu:
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")
}
}
}Ponizej widok dla kazdego wiersza, wyodrebniony jako osobny komponent dla czytelnosci:
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)
}
}Wyodrebnienie InterviewRow do osobnej struktury sprawia, ze kod jest bardziej czytelny i mozliwy do ponownego uzycia. To ugruntowana najlepsza praktyka w SwiftUI.
Dodawanie Akcji: Usuwanie i Zmiana Kolejnosci
Listy iOS natywnie obsluguja usuwanie (przesuniecie w lewo) i zmiane kolejnosci (przeciagnij i upusc). SwiftUI czyni to trywialne dzieki .onDelete i .onMove:
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)
}
}Zaledwie 4 dodatkowe linie kodu (.onDelete, .onMove, EditButton i dwie funkcje) wystarczaja, aby uzyskac w pelni interaktywna liste. To wlasnie moc SwiftUI.
Animowanie Interfejsow
SwiftUI wyroznia sie w dziedzinie animacji. W przeciwienstwie do UIKit, gdzie animacje wymagaly duzej ilosci kodu, tutaj wszystko jest deklaratywne: opisuje sie stan koncowy, a SwiftUI animuje przejscie.
Animacje Niejawne z withAnimation
Najprostszym sposobem na animowanie jest opakowanie zmiany stanu w withAnimation. SwiftUI wykrywa, co sie zmienia, i automatycznie animuje dotkniety wlasciwosci wizualne.
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)
}
}
}Po tapnieciu przycisku isExpanded zmienia wartosc. Dzieki withAnimation zmiana rozmiaru prostokata jest animowana z efektem sprezyny. Nie trzeba okreslac, co ma byc animowane -- SwiftUI ustala to samodzielnie.
Przejscia: Animowanie Pojawiania sie i Znikania
Przejscia (transitions) okreslaja, jak widok pojawia sie lub znika. Domyslnie jest to zanikanie (opacity), ale mozna je dostosowac:
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)
}
}W tym przykladzie karta pojawia sie przez powiekszenie ze srodka (scale + opacity) i znika przesuwajac sie w bok (slide). Te mikro-animacje nadaja interfejsowi zywy i profesjonalny charakter.
SwiftUI automatycznie optymalizuje animacje. Zalecane jest stosowanie .spring() dla naturalnego efektu i unikanie zbyt dlugich animacji (> 0.5s), ktore frustruja uzytkownikow.
Ladowanie Danych Asynchronicznych
W rzeczywistych aplikacjach dane czesto pochodza z API. Swift Concurrency (async/await) integruje sie bezproblemowo ze SwiftUI poprzez modyfikator .task.
Wzorzec Loading / Error / Success
Ponizej standardowy wzorzec wyswietlania danych z API. Obslugiwane sa trzy stany: ladowanie, blad i sukces.
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
}
}Modyfikator .task jest kluczowy: uruchamia asynchroniczne zadanie, gdy widok sie pojawia, i automatycznie je anuluje, gdy widok znika. Wycieki pamieci sa tym samym wykluczone.
Podsumowanie
SwiftUI stal sie niezbednym narzedziem we wspolczesnym rozwoju iOS. Wraz z iOS 18 framework osiagnal dojrzalosc, ktora czyni go w pelni odpowiednim do aplikacji produkcyjnych.
Kluczowe Wnioski
- Paradygmat deklaratywny: opisywanie tego, co jest porzadane, zamiast sposobu budowy
- @State i @Binding: reaktywne zarzadzanie stanem i jego propagacja miedzy widokami
- Stacki: laczenie VStack, HStack i ZStack w elastyczne layouty
- List: wyswietlanie kolekcji przy minimalnej ilosci kodu z natywnymi interakcjami
- Animacje: uzycie
withAnimationdo automatycznych plynnych przejsc - Async/await: ladowanie danych za pomoca
.taski obsluga stanow ladowania/bledu
Checklist
- Zrozumienie roznicy miedzy podejsciem imperatywnym (UIKit) a deklaratywnym (SwiftUI)
- Opanowanie modyfikatorow i ich kolejnosci stosowania
- Wiedza, kiedy stosowac @State vs @Binding vs @Observable
- Budowanie layoutow za pomoca Stackow
- Implementacja list z akcjami (usuwanie, przenoszenie)
- Animowanie zmian stanu za pomoca withAnimation
Zacznij ćwiczyć!
Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.
SwiftUI otwiera drzwi do tworzenia eleganckich aplikacji na caly ekosystem Apple. Najlepszym sposobem nauki jest praktyka: warto stworzyc maly osobisty projekt i eksperymentowac z kazdym konceptem przedstawionym w tym artykule.
Tagi
Udostępnij
Powiązane artykuły

Wydajność SwiftUI: Optymalizacja LazyVStack i Złożonych List
Techniki optymalizacji LazyVStack i list SwiftUI. Zmniejszanie zużycia pamięci, poprawa wydajności przewijania i unikanie typowych pułapek.

Niestandardowe ViewModifiers w SwiftUI: wzorce wielokrotnego użytku dla design systemów
Buduj niestandardowe ViewModifiers w SwiftUI dla spójnego design systemu. Wzorce, najlepsze praktyki i praktyczne przykłady efektywnego stylowania widoków iOS.

SwiftUI @Observable vs @State: Kiedy Czego Używać w 2026
Opanuj różnice między @Observable a @State w SwiftUI, aby wybrać odpowiednie narzędzie do zarządzania stanem w aplikacjach iOS.