Project 14 — Bucket List
Bucket List хранит памятные места на карте. Пользователь добавляет annotation, редактирует название и описание, получает сведения о nearby places и открывает данные после Face ID или Touch ID.
Готовность к проекту
0 из 6 рекомендуемых тем изучено. Проект всегда доступен — это не ограничение.
Рабочий vertical slice
import SwiftUI
import MapKit
import LocalAuthentication
struct Location: Identifiable {
let id = UUID()
let name: String
let coordinate: CLLocationCoordinate2D
}
struct ContentView: View {
@State private var isUnlocked = false
@State private var message = "Откройте список мест"
private let locations = [
Location(name: "Эрмитаж", coordinate: .init(latitude: 59.9398, longitude: 30.3146))
]
var body: some View {
Group {
if isUnlocked {
Map {
ForEach(locations) { location in
Marker(location.name, coordinate: location.coordinate)
}
}
} else {
ContentUnavailableView {
Label("Места закрыты", systemImage: "lock")
} description: {
Text(message)
} actions: {
Button("Открыть") { Task { await authenticate() } }
.buttonStyle(.borderedProminent)
}
}
}
}
@MainActor
private func authenticate() async {
let context = LAContext()
var error: NSError?
guard context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) else {
message = "На устройстве не настроена аутентификация"
return
}
do {
isUnlocked = try await context.evaluatePolicy(
.deviceOwnerAuthentication,
localizedReason: "Открыть сохранённые места"
)
} catch {
message = "Не удалось открыть места"
}
}
}Добавьте NSFaceIDUsageDescription перед запуском на устройстве. Этот slice показывает честный locked/unlocked flow; добавление и сохранение точек идут следующим шагом.
Location как value type
struct Location: Identifiable, Codable, Equatable {
let id: UUID
var name: String
var description: String
let latitude: Double
let longitude: Double
}Map показывает Array locations, а Binding к выбранной location позволяет редактировать копию и затем заменить соответствующий элемент по id.
Map и annotations
MapReader { proxy in
Map {
ForEach(locations) { location in
Annotation(location.name, coordinate: location.coordinate) {
Image(systemName: "star.circle")
}
}
}
}Gesture position преобразуется в coordinate через MapProxy. Проверяй Optional результат преобразования.
Файл вместо UserDefaults
JSON Array сохраняется в documents directory через Data.write(to:options:). Опция .completeFileProtection шифрует файл, когда устройство заблокировано.
Biometrics
LAContext сначала проверяет доступность политики, затем асинхронно оценивает её. Политика .deviceOwnerAuthentication разрешает системный fallback на код-пароль; вариант WithBiometrics допускает только биометрию. Любая ветка — успех, отказ, отмена — должна обновить UI на main actor.
if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) {
let success = try await context.evaluatePolicy(.deviceOwnerAuthentication,
localizedReason: "Откройте сохранённые места")
isUnlocked = success
}Для Face ID добавьте NSFaceIDUsageDescription в конфигурацию приложения.
Глубже: границы защиты
.completeFileProtection защищает файл на заблокированном устройстве. Экран аутентификации сам по себе не шифрует уже прочитанные данные в памяти. Для учебного проекта достаточно различать эти два механизма; полноценную security model проектируют отдельно.
Испытание
Задание
Добавь сортировку locations, обработку недоступной biometrics и понятный empty state карты.Что уже нужно знать
Protocols, Comparable, errors, optionals, Codable и MapKit.Показать подсказку 1
Показать подсказку 2
Показать решение
static func < (lhs: Location, rhs: Location) -> Bool {
lhs.name.localizedStandardCompare(rhs.name) == .orderedAscending
}Что ты сейчас использовал
Swift Знакомые основы
SwiftUI Новое в проекте
- + Codable
- + MapKit
- + Biometrics
- + FileManager
Дальше проверим, доступен ли интерфейс разным пользователям: Accessibility →.
Комментарии
Вопросы, находки и подсказки по этой странице.
Войди, чтобы задать вопрос или помочь другим.