Продуктивність SwiftUI: Оптимізація LazyVStack та Складних Списків
Техніки оптимізації для LazyVStack та списків SwiftUI. Зменшення споживання пам'яті, покращення продуктивності прокручування та уникнення поширених помилок.

Списки представляють один із найчастіше використовуваних компонентів у застосунках iOS. LazyVStack та List від SwiftUI пропонують продуктивні рішення для відображення колекцій даних, але неправильне використання може швидко погіршити користувацький досвід. Розуміння внутрішніх механізмів цих компонентів допомагає уникнути поширених пасток і будувати плавні інтерфейси.
Стаття представляє основні техніки оптимізації списків SwiftUI: lazy loading, переробку View, керування ідентифікаторами та просунуті патерни для великих наборів даних. Оновлено для iOS 27 з новими API для зміни порядку та swipe-дій.
Розуміння Lazy Loading у SwiftUI
Lazy loading створює View лише тоді, коли вони стають видимими на екрані. На відміну від VStack, що створює всі дочірні View одразу, LazyVStack відкладає це створення, різко зменшуючи споживання пам'яті та час початкового рендерингу.
import SwiftUI
// ❌ Problem: VStack instantiates all 10,000 views immediately
struct NonLazyListView: View {
let items = (1...10000).map { "Item (/bin/bash)" }
var body: some View {
ScrollView {
VStack {
ForEach(items, id: .self) { item in
// Each view is created at launch
ExpensiveRowView(title: item)
}
}
}
}
}
// ✅ Solution: LazyVStack creates views on demand
struct LazyListView: View {
let items = (1...10000).map { "Item (/bin/bash)" }
var body: some View {
ScrollView {
LazyVStack {
ForEach(items, id: .self) { item in
// Only visible views are created
ExpensiveRowView(title: item)
}
}
}
}
}Різниця у продуктивності стає значною вже при кількох сотнях елементів. З 10 000 елементами VStack може запускатися декілька секунд, тоді як LazyVStack залишається миттєвим.
Вимірювання Впливу Lazy Loading
Instruments дозволяє точно вимірювати використання пам'яті та CPU. Ось тестова View, що ілюструє різницю:
struct ExpensiveRowView: View {
let title: String
// Simulating expensive initialization
init(title: String) {
self.title = title
// Log to visualize when the view is created
print("Creating row: (title)")
}
var body: some View {
HStack {
// Image with processing
Circle()
.fill(
LinearGradient(
colors: [.blue, .purple],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.frame(width: 50, height: 50)
VStack(alignment: .leading) {
Text(title)
.font(.headline)
Text("Subtitle with computation")
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
}
.padding()
}
}При запуску з VStack усі 10 000 логів з'являються одразу. З LazyVStack спочатку логуються лише видимі елементи (приблизно 15-20 залежно від розміру екрана), а інші з'являються під час прокручування.
LazyVStack зберігає створені View у пам'яті після їх появи. На відміну від List, який активно переробляє комірки, View у LazyVStack зберігаються до знищення батьківського компонента.
Важливість Стабільних Ідентифікаторів
Ідентифікатори утворюють центральний механізм оновлень списків SwiftUI. Нестабільний ідентифікатор спричиняє непотрібні перестворення View і може викликати візуальні баги, такі як неправильні анімації чи втрату позиції прокручування.
// ❌ Problem: using index as identifier
struct UnstableIdentifierView: View {
@State private var items = ["A", "B", "C", "D"]
var body: some View {
List {
// Index changes if an element is deleted
ForEach(items.indices, id: .self) { index in
Text(items[index])
}
}
}
}
// ❌ Problem: using UUID() in ForEach
struct RegeneratedIdentifierView: View {
let items = ["A", "B", "C", "D"]
var body: some View {
List {
// UUID() generates a new ID on each render
ForEach(items, id: .self) { item in
// Subtle issue if items contain duplicates
Text(item)
}
}
}
}
// ✅ Solution: model with stable identifier
struct Item: Identifiable {
let id: UUID // Created once
var name: String
init(name: String) {
self.id = UUID()
self.name = name
}
}
struct StableIdentifierView: View {
@State private var items = [
Item(name: "A"),
Item(name: "B"),
Item(name: "C"),
Item(name: "D")
]
var body: some View {
List {
// id is stable for the item's lifetime
ForEach(items) { item in
Text(item.name)
}
}
}
}Використання унікального та постійного ідентифікатора гарантує, що SwiftUI коректно розрізняє елементи під час оновлень, анімацій та порівнянь.
Оптимізація Комірок з Equatable
SwiftUI порівнює View, щоб визначити, чи потрібен повторний рендеринг. За замовчуванням це порівняння використовує reflection, що може бути дорогим. Реалізація Equatable забезпечує оптимізоване та явне порівняння.
// Data model
struct Contact: Identifiable, Equatable {
let id: UUID
var name: String
var email: String
var avatarURL: URL?
var lastActivity: Date
// Custom comparison: ignore lastActivity
// if other properties are identical
static func == (lhs: Contact, rhs: Contact) -> Bool {
lhs.id == rhs.id &&
lhs.name == rhs.name &&
lhs.email == rhs.email &&
lhs.avatarURL == rhs.avatarURL
// lastActivity intentionally excluded
}
}
// Optimized cell view
struct ContactRow: View, Equatable {
let contact: Contact
// Explicit comparison to avoid unnecessary re-renders
static func == (lhs: ContactRow, rhs: ContactRow) -> Bool {
lhs.contact == rhs.contact
}
var body: some View {
HStack(spacing: 12) {
// Async avatar
AsyncImage(url: contact.avatarURL) { phase in
switch phase {
case .success(let image):
image
.resizable()
.aspectRatio(contentMode: .fill)
case .failure:
Image(systemName: "person.circle.fill")
.foregroundStyle(.gray)
default:
ProgressView()
}
}
.frame(width: 44, height: 44)
.clipShape(Circle())
// Contact information
VStack(alignment: .leading, spacing: 2) {
Text(contact.name)
.font(.body.weight(.medium))
Text(contact.email)
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
}
.padding(.vertical, 4)
}
}
// List using EquatableView
struct ContactListView: View {
let contacts: [Contact]
var body: some View {
List {
ForEach(contacts) { contact in
// EquatableView prevents re-renders if contact unchanged
EquatableView(content: ContactRow(contact: contact))
}
}
}
}Ця оптимізація значно знижує навантаження на CPU під час швидкого прокручування, особливо для комірок із обчисленнями чи зображеннями.
Готовий до співбесід з iOS?
Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.
Керування Асинхронним Завантаженням Зображень
Зображення часто стають вузьким місцем продуктивності у списках. iOS 27 представив стандартне HTTP-кешування для AsyncImage за замовчуванням, усуваючи потребу в ручному кешуванні в багатьох сценаріях. Для застосунків, орієнтованих на iOS 26 та раніші версії, або тих, що потребують тонкого контролю над поведінкою кешування, власна реалізація залишається цінною.
import SwiftUI
// Singleton image cache (for iOS 26 or custom caching needs)
actor ImageCache {
static let shared = ImageCache()
private var cache = NSCache<NSString, UIImage>()
private init() {
// Memory limit: 50 MB
cache.totalCostLimit = 50 * 1024 * 1024
}
func image(for url: URL) -> UIImage? {
cache.object(forKey: url.absoluteString as NSString)
}
func setImage(_ image: UIImage, for url: URL) {
// Cost estimation: image bytes
let cost = Int(image.size.width * image.size.height * 4)
cache.setObject(image, forKey: url.absoluteString as NSString, cost: cost)
}
}
// Optimized image view with caching
struct CachedAsyncImage: View {
let url: URL?
let size: CGSize
@State private var image: UIImage?
@State private var isLoading = false
var body: some View {
Group {
if let image {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fill)
} else if isLoading {
Rectangle()
.fill(Color.gray.opacity(0.2))
.overlay(ProgressView())
} else {
Rectangle()
.fill(Color.gray.opacity(0.2))
}
}
.frame(width: size.width, height: size.height)
.clipped()
.task(id: url) {
await loadImage()
}
}
private func loadImage() async {
guard let url else { return }
// Check cache
if let cached = await ImageCache.shared.image(for: url) {
self.image = cached
return
}
isLoading = true
defer { isLoading = false }
// Download and resize
do {
let (data, _) = try await URLSession.shared.data(from: url)
// Resize to save memory
if let original = UIImage(data: data),
let resized = await resizeImage(original, to: size) {
await ImageCache.shared.setImage(resized, for: url)
self.image = resized
}
} catch {
// Handle error silently
}
}
private func resizeImage(_ image: UIImage, to size: CGSize) async -> UIImage? {
// Use screen scale
let scale = await UIScreen.main.scale
let targetSize = CGSize(
width: size.width * scale,
height: size.height * scale
)
return await withCheckedContinuation { continuation in
DispatchQueue.global(qos: .userInitiated).async {
let renderer = UIGraphicsImageRenderer(size: targetSize)
let resized = renderer.image { _ in
image.draw(in: CGRect(origin: .zero, size: targetSize))
}
continuation.resume(returning: resized)
}
}
}
}Для iOS 27 та новіших версій стандартний AsyncImage автоматично враховує HTTP-заголовки кешу. Власний URLCache можна застосувати через модифікатор asyncImageURLSession(_:) для специфічних вимог.
Розумний Prefetching для Передбачення
Для дуже довгих списків prefetching завантажує зображення до того, як вони стануть видимими:
// Prefetching coordinator
@Observable
final class ImagePrefetcher {
private var prefetchTasks: [URL: Task<Void, Never>] = [:]
private let prefetchDistance = 10 // Number of items ahead
func prefetchImages(for items: [Contact], visibleRange: Range<Int>) {
// Calculate prefetch range
let prefetchStart = max(0, visibleRange.lowerBound - prefetchDistance)
let prefetchEnd = min(items.count, visibleRange.upperBound + prefetchDistance)
// Launch prefetch for items in range
for index in prefetchStart..<prefetchEnd {
guard let url = items[index].avatarURL else { continue }
// Avoid duplicates
guard prefetchTasks[url] == nil else { continue }
prefetchTasks[url] = Task {
// Check if already cached
if await ImageCache.shared.image(for: url) != nil {
return
}
// Prefetch
do {
let (data, _) = try await URLSession.shared.data(from: url)
if let image = UIImage(data: data) {
await ImageCache.shared.setImage(image, for: url)
}
} catch {
// Ignore prefetch errors
}
}
}
// Cancel out-of-range prefetches
cancelOutOfRangePrefetches(validRange: prefetchStart..<prefetchEnd, items: items)
}
private func cancelOutOfRangePrefetches(validRange: Range<Int>, items: [Contact]) {
let validURLs = Set(
items[validRange].compactMap { /bin/bash.avatarURL }
)
for (url, task) in prefetchTasks {
if !validURLs.contains(url) {
task.cancel()
prefetchTasks.removeValue(forKey: url)
}
}
}
}Вибір між List та LazyVStack
iOS 27 значно скоротив розрив між List та LazyVStack. Раніше List був єдиним варіантом для нативних swipe-дій та перетягування для зміни порядку. Нові API .swipeActionsContainer() та .reorderable() переносять ці можливості до будь-якого контейнера.
// ✅ List: automatic cell recycling, built-in styles
// Best for: standard contacts, settings, data tables
struct ContactsWithSwipeActions: View {
@State private var contacts: [Contact] = []
var body: some View {
List {
ForEach(contacts) { contact in
ContactRow(contact: contact)
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
deleteContact(contact)
} label: {
Label("Delete", systemImage: "trash")
}
}
.swipeActions(edge: .leading) {
Button {
favoriteContact(contact)
} label: {
Label("Favorite", systemImage: "star")
}
.tint(.yellow)
}
}
}
.listStyle(.plain)
}
private func deleteContact(_ contact: Contact) {
contacts.removeAll { /bin/bash.id == contact.id }
}
private func favoriteContact(_ contact: Contact) {
// Favorite logic
}
}
// ✅ LazyVStack with iOS 27 swipe actions
// Best for: custom layouts, cards, feeds
struct CustomFeedWithSwipeActions: View {
@State private var posts: [Post] = []
var body: some View {
ScrollView {
LazyVStack(spacing: 16) {
ForEach(posts) { post in
PostCard(post: post)
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
deletePost(post)
} label: {
Label("Delete", systemImage: "trash")
}
}
}
}
.padding(.horizontal)
.swipeActionsContainer()
}
}
private func deletePost(_ post: Post) {
posts.removeAll { /bin/bash.id == post.id }
}
}Вибір тепер залежить від візуальних вимог, а не від доступності функцій:
| Вимога | Рекомендований контейнер |
|---|---|
| Переробка комірок для ефективності пам'яті | List |
| Власні відступи та padding | LazyVStack |
| Вбудовані роздільники та відступи | List |
| Картки або нестандартні макети | LazyVStack |
| Заголовки секцій із sticky-поведінкою | Обидва (використовуйте pinnedViews) |
| Swipe-дії | Обидва (iOS 27+) |
| Перетягування для зміни порядку | Обидва (iOS 27+) |
List активно переробляє комірки, що може спричинити проблеми з локальним станом (@State). Значення @State у комірках List можуть бути несподівано перевикористані. Краще зберігати стан у моделі даних або у ViewModel.
Зміна Порядку Елементів у LazyVStack (iOS 27)
До iOS 27 перетягування для зміни порядку вимагало List з onMove(perform:). Нові reorderable API працюють з будь-яким контейнером, включаючи LazyVStack та LazyVGrid.
import SwiftUI
struct ReorderableCardList: View {
@State private var cards: [Card] = Card.sampleData
var body: some View {
ScrollView {
LazyVStack(spacing: 12) {
ForEach(cards) { card in
CardView(card: card)
}
.reorderable() // Mark content as draggable
}
.padding()
.reorderContainer(for: Card.self) { difference in
// Apply the reorder to the model
cards.apply(difference: difference)
}
}
}
}
struct Card: Identifiable {
let id: UUID
var title: String
var color: Color
static var sampleData: [Card] {
[
Card(id: UUID(), title: "Design Review", color: .blue),
Card(id: UUID(), title: "Code Sprint", color: .green),
Card(id: UUID(), title: "Testing Phase", color: .orange)
]
}
}
struct CardView: View {
let card: Card
var body: some View {
Text(card.title)
.font(.headline)
.frame(maxWidth: .infinity)
.padding()
.background(card.color.opacity(0.2))
.cornerRadius(12)
}
}
// Extension to apply ReorderDifference
extension Array where Element: Identifiable {
mutating func apply(difference: ReorderDifference<Element.ID>) {
// Move items from sources to destination
let movedItems = difference.sources.compactMap { id in
self.first { /bin/bash.id == id }
}
// Remove from original positions
self.removeAll { item in
difference.sources.contains(item.id)
}
// Insert at destination
if let destinationIndex = self.firstIndex(where: { /bin/bash.id == difference.destination }) {
self.insert(contentsOf: movedItems, at: destinationIndex)
} else {
self.append(contentsOf: movedItems)
}
}
}SwiftUI обробляє попередній перегляд перетягування, placeholder вставки та анімацію опускання. Об'єкт ReorderDifference надає ID джерел та місце призначення, залишаючи оновлення моделі коду застосунку.
Оптимізовані Секції та Заголовки
Організація вмісту у секції покращує читабельність, але може впливати на продуктивність при поганій реалізації. Закріплені заголовки та керування секціями вимагають особливої уваги.
// Grouped data model
struct GroupedContacts {
let letter: String
let contacts: [Contact]
}
// View with optimized sections
struct SectionedContactList: View {
let groupedContacts: [GroupedContacts]
var body: some View {
ScrollView {
LazyVStack(spacing: 0, pinnedViews: [.sectionHeaders]) {
ForEach(groupedContacts, id: .letter) { group in
Section {
// Section content
ForEach(group.contacts) { contact in
ContactRow(contact: contact)
.padding(.horizontal)
.padding(.vertical, 8)
// Custom separator
if contact.id != group.contacts.last?.id {
Divider()
.padding(.leading, 68)
}
}
} header: {
// Optimized pinned header
SectionHeader(title: group.letter)
}
}
}
}
}
}
// Lightweight header for performance
struct SectionHeader: View {
let title: String
var body: some View {
Text(title)
.font(.headline)
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal)
.padding(.vertical, 8)
.background(.ultraThinMaterial)
}
}
// Optimized grouping function
extension Array where Element == Contact {
func groupedByFirstLetter() -> [GroupedContacts] {
// Dictionary for O(n) grouping
var groups: [String: [Contact]] = [:]
for contact in self {
let letter = String(contact.name.prefix(1)).uppercased()
groups[letter, default: []].append(contact)
}
// Sort groups alphabetically
return groups
.map { GroupedContacts(letter: /bin/bash.key, contacts: /bin/bash.value) }
.sorted { /bin/bash.letter < .letter }
}
}Закріплені заголовки (pinnedViews: [.sectionHeaders]) залишаються видимими під час прокручування, покращуючи навігацію у довгих списках.
Пагінація та Нескінченне Прокручування
Для великих наборів даних пагінація уникає завантаження всіх даних у пам'ять. Реалізація має бути прозорою для користувача.
// ViewModel handling pagination
@Observable
final class PaginatedListViewModel {
private(set) var items: [Contact] = []
private(set) var isLoading = false
private(set) var hasMorePages = true
private var currentPage = 0
private let pageSize = 20
private let dataService: ContactDataService
init(dataService: ContactDataService) {
self.dataService = dataService
}
func loadInitialData() async {
guard items.isEmpty else { return }
await loadNextPage()
}
func loadMoreIfNeeded(currentItem: Contact) async {
// Trigger loading when approaching the end
guard let index = items.firstIndex(where: { /bin/bash.id == currentItem.id }) else {
return
}
// Load 5 items before the end
let thresholdIndex = items.count - 5
if index >= thresholdIndex {
await loadNextPage()
}
}
private func loadNextPage() async {
guard !isLoading, hasMorePages else { return }
isLoading = true
defer { isLoading = false }
do {
let newItems = try await dataService.fetchContacts(
page: currentPage,
limit: pageSize
)
items.append(contentsOf: newItems)
currentPage += 1
hasMorePages = newItems.count == pageSize
} catch {
// Handle error
}
}
}
// View with infinite scroll
struct InfiniteContactList: View {
@State private var viewModel: PaginatedListViewModel
init(dataService: ContactDataService) {
_viewModel = State(initialValue: PaginatedListViewModel(dataService: dataService))
}
var body: some View {
List {
ForEach(viewModel.items) { contact in
ContactRow(contact: contact)
.task {
// Check if more loading needed
await viewModel.loadMoreIfNeeded(currentItem: contact)
}
}
// Loading indicator at end of list
if viewModel.isLoading {
HStack {
Spacer()
ProgressView()
Spacer()
}
.padding()
}
}
.task {
await viewModel.loadInitialData()
}
}
}
// Protocol for data service
protocol ContactDataService {
func fetchContacts(page: Int, limit: Int) async throws -> [Contact]
}Цей патерн забезпечує плавне завантаження без блокування інтерфейсу та дозволяє ефективно керувати пам'яттю.
Готовий до співбесід з iOS?
Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.
Профілювання з Instruments
Виявлення проблем продуктивності вимагає точних інструментів вимірювання. Instruments пропонує кілька шаблонів, придатних для SwiftUI.
// Measurement points for debugging
struct PerformanceMonitor {
// Measure view creation time
static func measureViewCreation<T: View>(
_ name: String,
@ViewBuilder content: () -> T
) -> T {
let start = CFAbsoluteTimeGetCurrent()
let view = content()
let elapsed = CFAbsoluteTimeGetCurrent() - start
#if DEBUG
if elapsed > 0.016 { // More than 16ms = frame drop
print("[(name)] View creation took (elapsed * 1000)ms")
}
#endif
return view
}
}
// Extension to trace renders
extension View {
func debugRender(_ label: String) -> some View {
#if DEBUG
let _ = Self._printChanges()
print("Rendering: (label)")
#endif
return self
}
func measureRender(_ label: String) -> some View {
modifier(RenderMeasureModifier(label: label))
}
}
struct RenderMeasureModifier: ViewModifier {
let label: String
@State private var renderCount = 0
func body(content: Content) -> some View {
content
.onAppear {
renderCount += 1
#if DEBUG
print("[(label)] Render count: (renderCount)")
#endif
}
}
}Чек-лист Оптимізації з Instruments
Для ефективного профілювання списків SwiftUI:
- Time Profiler: визначте функції з найбільшим споживанням CPU
- Allocations: перевірте зростання пам'яті під час прокручування
- SwiftUI Instrument: візуалізуйте обчислення body
- Core Animation: виявляйте втрати кадрів
// Instrumented view for profiling
struct ProfiledContactList: View {
let contacts: [Contact]
var body: some View {
let _ = Self._printChanges() // Shows changes triggering re-render
List {
ForEach(contacts) { contact in
ContactRow(contact: contact)
.measureRender("ContactRow-(contact.id)")
}
}
}
}Цей API дебагу SwiftUI друкує у консоль, які властивості змінилися та спричинили повторне обчислення body. Незамінний для виявлення непотрібних рендерингів.
Просунуті Оптимізації з drawingGroup
Для складних View з багатьма візуальними ефектами drawingGroup() може значно покращити продуктивність, растеризуючи View у шар Metal.
// Cell with complex visual effects
struct ComplexVisualRow: View {
let item: Item
var body: some View {
HStack(spacing: 16) {
// Circle with gradient and shadow
Circle()
.fill(
RadialGradient(
colors: [.blue, .purple, .pink],
center: .center,
startRadius: 0,
endRadius: 25
)
)
.frame(width: 50, height: 50)
.shadow(color: .purple.opacity(0.5), radius: 8, y: 4)
VStack(alignment: .leading, spacing: 4) {
Text(item.name)
.font(.headline)
// Progress bar with gradient
GeometryReader { geometry in
Capsule()
.fill(Color.gray.opacity(0.2))
.overlay(alignment: .leading) {
Capsule()
.fill(
LinearGradient(
colors: [.green, .yellow, .orange],
startPoint: .leading,
endPoint: .trailing
)
)
.frame(width: geometry.size.width * item.progress)
}
}
.frame(height: 8)
}
}
.padding()
// Rasterization for performance
.drawingGroup()
}
}
// List using optimized cells
struct OptimizedComplexList: View {
let items: [Item]
var body: some View {
ScrollView {
LazyVStack(spacing: 8) {
ForEach(items) { item in
ComplexVisualRow(item: item)
}
}
.padding()
}
}
}
struct Item: Identifiable {
let id: UUID
let name: String
let progress: Double
}drawingGroup() особливо ефективний для View, що поєднують градієнти, тіні та ефекти розмиття.
Джерела
- WWDC26 SwiftUI Guide - Офіційна документація Apple щодо оновлень SwiftUI в iOS 27, включаючи кешування AsyncImage та API reorderable
- New SwiftUI APIs for Reordering and Drag and Drop on iOS 27 - Детальне пояснення модифікаторів
.reorderable()та.reorderContainer(for:) - What's New in SwiftUI for iOS 27 - Комплексний огляд змін SwiftUI з WWDC26
Висновок
Оптимізація списків SwiftUI ґрунтується на глибокому розумінні механізмів lazy loading, переробки та порівняння View. iOS 27 приніс значні покращення з нативним кешуванням AsyncImage та незалежними від контейнера swipe-діями та API для зміни порядку.
Чек-лист Продуктивності SwiftUI
- Використовуйте
LazyVStackабоListзамістьVStackдля колекцій - Реалізуйте
Identifiableзі стабільними, унікальними ID - Застосовуйте
Equatableдля складних комірок - Використовуйте нативне кешування AsyncImage в iOS 27 або реалізуйте власне кешування для раніших версій
- Попередньо завантажуйте дані з розумним prefetching
- Обирайте
Listдля переробки комірок абоLazyVStackдля кастомних розкладок (обидва тепер підтримують swipe-дії та зміну порядку) - Використовуйте
pinnedViewsдля заголовків секцій - Реалізуйте пагінацію для великих наборів даних
- Регулярно профілюйте з Instruments
- Застосовуйте
drawingGroup()до View зі складними візуальними ефектами
Починай практикувати!
Перевір свої знання з нашими симуляторами співбесід та технічними тестами.
Чи знайдеш ти помилку в iOS?
Справжній фрагмент коду, прихована помилка, одна спроба на день. Щоб спробувати, акаунт не потрібен.

Автор:
Anthony Fillion-MailletЗасновник SharpSkill
Fullstack-розробник понад 10 років. Керує SharpSkill і відповідає за все, що тут публікується.
Оновлено 19 серпня 2026 р.
Теги
Поділитися
Пов'язані статті

Користувацькі ViewModifier у SwiftUI: патерни багаторазового використання для design system
Створюйте користувацькі ViewModifier у SwiftUI для узгодженого design system. Патерни, кращі практики та практичні приклади ефективного стилізування iOS view.

SwiftUI @Observable vs @State: Коли Що Використовувати у 2026
Опануйте відмінності між @Observable та @State у SwiftUI, щоб обрати правильний інструмент керування станом для застосунків iOS.

CloudKit зі SwiftUI у 2026: патерни синхронізації між пристроями
Повний посібник із впровадження синхронізації CloudKit зі SwiftUI: CKSyncEngine, інтеграція зі SwiftData, розв'язання конфліктів і найкращі практики для iOS 2026.