Files
Nearfield_Friends/Proximity/Views/PresenceView.swift

145 lines
4.7 KiB
Swift
Raw Permalink Normal View History

import SwiftUI
/// The main presence screen.
///
/// This is the emotional core of the app. A large, tactile "I am here"
/// toggle that makes being present feel like an intentional act not a
/// passive background process. Below it, the live feed of silhouettes.
struct PresenceView: View {
@EnvironmentObject private var vm: PresenceViewModel
@EnvironmentObject private var revealFlow: RevealFlow
@State private var showReveal = false
var body: some View {
NavigationStack {
ScrollView {
VStack(spacing: 24) {
presenceToggle
radiusPicker
encounterFeed
}
.padding()
}
.navigationTitle("Proximity")
.toolbar {
#if DEBUG
ToolbarItem(placement: .topBarLeading) {
NavigationLink {
DebugMenu()
} label: {
Image(systemName: "hammer")
}
}
#endif
}
}
.onChange(of: revealFlow.phase) { _, phase in
if case .mutual = phase {
showReveal = true
}
}
.fullScreenCover(isPresented: $showReveal) {
RevealView()
.environmentObject(revealFlow)
}
}
// MARK: - The presence toggle
private var presenceToggle: some View {
Button(action: vm.togglePresence) {
ZStack {
Circle()
.fill(vm.isPresent ? Color.green : Color.gray.opacity(0.2))
.frame(width: 200, height: 200)
.overlay(
Circle()
.stroke(vm.isPresent ? Color.green : Color.gray,
lineWidth: 4)
)
VStack(spacing: 8) {
Image(systemName: vm.isPresent
? "dot.radiowaves.left.and.right"
: "person.crop.circle.dashed")
.font(.system(size: 56))
Text(vm.isPresent ? "I'm Here" : "Tap to be Present")
.font(.headline)
}
.foregroundColor(vm.isPresent ? .white : .secondary)
}
.scaleEffect(vm.isPresent ? 1.0 : 0.98)
.animation(.spring(response: 0.4, dampingFraction: 0.6),
value: vm.isPresent)
}
.buttonStyle(.plain)
.accessibilityLabel(vm.isPresent ? "You are present. Tap to go offline."
: "Go present")
}
// MARK: - Radius picker
private var radiusPicker: some View {
VStack(alignment: .leading, spacing: 8) {
Text("How far are you open to?")
.font(.subheadline)
.foregroundColor(.secondary)
Picker("Radius", selection: $vm.radiusTier) {
ForEach(DistanceTier.allCases) { tier in
Text(tier.title).tag(tier)
}
}
.pickerStyle(.segmented)
Text(vm.radiusTier.subtitle)
.font(.caption)
.foregroundColor(.secondary)
}
}
// MARK: - Encounter feed
private var encounterFeed: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Text("People you've passed")
.font(.headline)
Spacer()
if let last = vm.lastScan {
Text("last scan \(last, style: .time)")
.font(.caption)
.foregroundColor(.secondary)
}
}
if vm.encounters.isEmpty {
emptyState
} else {
ForEach(vm.encounters) { encounter in
SilhouetteRow(encounter: encounter) {
// Tap a silhouette consider it wave.
revealFlow.consider(encounter)
Task { await revealFlow.wave() }
}
}
}
}
}
private var emptyState: some View {
VStack(spacing: 12) {
Image(systemName: "eye.slash")
.font(.system(size: 40))
.foregroundColor(.secondary)
Text(vm.isPresent
? "Looking for people near you…"
: "Go present to start seeing people.")
.multilineTextAlignment(.center)
.foregroundColor(.secondary)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 40)
}
}