Skip to content

Project 19 — SnowSeeker

SnowSeeker — каталог горнолыжных курортов. На iPhone он работает как обычный navigation stack, а на iPad использует master-detail layout. Проект собирает data modeling, search, favorites и adaptive design.

Перед началом

Готовность к проекту

0%

0 из 7 рекомендуемых тем изучено. Проект всегда доступен — это не ограничение.

Рабочий vertical slice

swift
import SwiftUI

struct Resort: Identifiable, Hashable {
    let id: Int
    let name: String
    let country: String
}

struct ContentView: View {
    private let resorts = [
        Resort(id: 1, name: "Шамони", country: "Франция"),
        Resort(id: 2, name: "Церматт", country: "Швейцария"),
        Resort(id: 3, name: "Роза Хутор", country: "Россия")
    ]
    @State private var selection: Resort?
    @State private var search = ""

    private var visibleResorts: [Resort] {
        search.isEmpty ? resorts : resorts.filter {
            $0.name.localizedStandardContains(search)
        }
    }

    var body: some View {
        NavigationSplitView {
            List(visibleResorts, selection: $selection) { resort in
                NavigationLink(value: resort) {
                    VStack(alignment: .leading) {
                        Text(resort.name)
                        Text(resort.country).foregroundStyle(.secondary)
                    }
                }
            }
            .searchable(text: $search)
            .navigationTitle("Курорты")
        } detail: {
            if let selection {
                VStack(spacing: 12) {
                    Text(selection.name).font(.largeTitle.bold())
                    Text(selection.country)
                }
            } else {
                ContentUnavailableView("Выберите курорт", systemImage: "snowflake")
            }
        }
    }
}

#Preview { ContentView() }

Проверьте узкое и широкое окно, пустой search result и выбор курорта. Favorites добавляйте после устойчивой navigation.

swift
@State private var selectedResort: Resort?

NavigationSplitView {
    List(resorts, selection: $selectedResort) { resort in
        NavigationLink(value: resort) { ResortRow(resort: resort) }
    }
} detail: {
    if let selectedResort {
        ResortView(resort: selectedResort)
    } else {
        ContentUnavailableView("Выберите курорт", systemImage: "snowflake")
    }
}

Optional selected model естественно описывает момент, когда пользователь ещё ничего не выбрал.

Search и sort

Search text и sort order — state. Computed property фильтрует и сортирует исходный Array, не создавая второго источника истины.

swift
var filteredResorts: [Resort] {
    let result = searchText.isEmpty ? resorts : resorts.filter {
        $0.name.localizedStandardContains(searchText)
    }
    return result.sorted(using: sortOrder)
}

Adaptive layout

horizontalSizeClass помогает выбрать компактное или просторное представление, но не привязывай решение к конкретной модели устройства. Окно iPad может стать узким в multitasking, а крупный Dynamic Type меняет полезную ширину. Для локального выбора компоновки предпочитайте ViewThatFits; проверяйте resizable window, split view, rotation и максимальный Dynamic Type.

Favorites

Отдельный observable favorites store хранит Set идентификаторов. Set быстро отвечает, находится ли курорт в избранном, а Codable сохраняет выбор локально.

Храните selection как стабильный Resort.ID, если состояние должно пережить перезапуск или deep link. После загрузки данных восстановите model по ID и покажите fallback, если запись была удалена.

Reusable details

Разбивай большой detail screen на небольшие View с явными inputs. Environment используй для действительно общих зависимостей, а не чтобы скрыть каждый параметр.

Самостоятельная работа

Испытание

Задание

Добавь filter по стране/размеру/цене, favorites-only режим и полноценный empty state для поиска.

Что уже нужно знать

Arrays, Sets, optionals, computed properties, navigation, search и adaptive layout.
Показать подсказку 1
Построй один pipeline: исходный Array → favorites filter → text filter → sort.
Показать подсказку 2
Не меняй исходный Array при поиске: иначе сброс запроса не восстановит данные.
Показать решение
swift
var visibleResorts: [Resort] {
    resorts
        .filter { !favoritesOnly || favorites.contains($0.id) }
        .filter { searchText.isEmpty || $0.name.localizedStandardContains(searchText) }
        .sorted { $0.name < $1.name }
}

Что ты сейчас использовал

SwiftUI Новое в проекте

  • + Navigation
  • + List
  • + Search
  • + Size classes

Ты прошёл последовательность от let и var до адаптивного SwiftUI-приложения. Заверши курс через Final Exam →.

Переход между соседними страницами: и вне полей ввода.

ОБСУЖДЕНИЕ

Комментарии

Вопросы, находки и подсказки по этой странице.

Загружаем комментарии…