initial commit: Proximity iOS app proposal, full Swift code scaffold, reference backend, testing docs, generated Xcode project
This commit is contained in:
144
Proximity/Views/PresenceView.swift
Normal file
144
Proximity/Views/PresenceView.swift
Normal file
@@ -0,0 +1,144 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user