1.0.0 for iOS is ready to ship

This commit is contained in:
2025-09-14 23:07:32 -06:00
parent a3e60ce995
commit 127c25f506
33 changed files with 2646 additions and 251 deletions

View File

@@ -1,9 +1,3 @@
//
// ProblemsView.swift
// OpenClimb
//
// Created by OpenClimb on 2025-01-17.
//
import SwiftUI
@@ -45,9 +39,13 @@ struct ProblemsView: View {
NavigationView {
VStack(spacing: 0) {
if !dataManager.problems.isEmpty {
FilterSection()
.padding()
.background(.regularMaterial)
FilterSection(
selectedClimbType: $selectedClimbType,
selectedGym: $selectedGym,
filteredProblems: filteredProblems
)
.padding()
.background(.regularMaterial)
}
if filteredProblems.isEmpty {
@@ -79,8 +77,9 @@ struct ProblemsView: View {
struct FilterSection: View {
@EnvironmentObject var dataManager: ClimbingDataManager
@State private var selectedClimbType: ClimbType?
@State private var selectedGym: Gym?
@Binding var selectedClimbType: ClimbType?
@Binding var selectedGym: Gym?
let filteredProblems: [Problem]
var body: some View {
VStack(spacing: 12) {
@@ -154,19 +153,6 @@ struct FilterSection: View {
}
}
private var filteredProblems: [Problem] {
var filtered = dataManager.problems
if let climbType = selectedClimbType {
filtered = filtered.filter { $0.climbType == climbType }
}
if let gym = selectedGym {
filtered = filtered.filter { $0.gymId == gym.id }
}
return filtered
}
}
struct FilterChip: View {
@@ -195,14 +181,47 @@ struct FilterChip: View {
struct ProblemsList: View {
let problems: [Problem]
@EnvironmentObject var dataManager: ClimbingDataManager
@State private var problemToDelete: Problem?
@State private var problemToEdit: Problem?
var body: some View {
List(problems) { problem in
NavigationLink(destination: ProblemDetailView(problemId: problem.id)) {
ProblemRow(problem: problem)
}
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button(role: .destructive) {
problemToDelete = problem
} label: {
Label("Delete", systemImage: "trash")
}
Button {
problemToEdit = problem
} label: {
Label("Edit", systemImage: "pencil")
}
.tint(.blue)
}
}
.alert("Delete Problem", isPresented: .constant(problemToDelete != nil)) {
Button("Cancel", role: .cancel) {
problemToDelete = nil
}
Button("Delete", role: .destructive) {
if let problem = problemToDelete {
dataManager.deleteProblem(problem)
problemToDelete = nil
}
}
} message: {
Text(
"Are you sure you want to delete this problem? This will also delete all associated attempts."
)
}
.sheet(item: $problemToEdit) { problem in
AddEditProblemView(problemId: problem.id)
}
.listStyle(.plain)
}
}
@@ -269,19 +288,10 @@ struct ProblemRow: View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 8) {
ForEach(problem.imagePaths.prefix(3), id: \.self) { imagePath in
AsyncImage(url: URL(fileURLWithPath: imagePath)) { image in
image
.resizable()
.aspectRatio(contentMode: .fill)
} placeholder: {
RoundedRectangle(cornerRadius: 8)
.fill(.gray.opacity(0.3))
}
.frame(width: 60, height: 60)
.clipped()
.cornerRadius(8)
ProblemImageView(imagePath: imagePath)
}
}
.padding(.horizontal, 4)
}
}
@@ -292,7 +302,7 @@ struct ProblemRow: View {
.fontWeight(.medium)
}
}
.padding(.vertical, 4)
.padding(.vertical, 8)
}
}
@@ -356,6 +366,70 @@ struct EmptyProblemsView: View {
}
}
struct ProblemImageView: View {
let imagePath: String
@State private var uiImage: UIImage?
@State private var isLoading = true
@State private var hasFailed = false
var body: some View {
Group {
if let uiImage = uiImage {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 60, height: 60)
.clipped()
.cornerRadius(8)
} else if hasFailed {
RoundedRectangle(cornerRadius: 8)
.fill(.gray.opacity(0.2))
.frame(width: 60, height: 60)
.overlay {
Image(systemName: "photo")
.foregroundColor(.gray)
.font(.title3)
}
} else {
RoundedRectangle(cornerRadius: 8)
.fill(.gray.opacity(0.3))
.frame(width: 60, height: 60)
.overlay {
ProgressView()
.scaleEffect(0.8)
}
}
}
.onAppear {
loadImage()
}
}
private func loadImage() {
guard !imagePath.isEmpty else {
hasFailed = true
isLoading = false
return
}
DispatchQueue.global(qos: .userInitiated).async {
if let data = ImageManager.shared.loadImageData(fromPath: imagePath),
let image = UIImage(data: data)
{
DispatchQueue.main.async {
self.uiImage = image
self.isLoading = false
}
} else {
DispatchQueue.main.async {
self.hasFailed = true
self.isLoading = false
}
}
}
}
}
#Preview {
ProblemsView()
.environmentObject(ClimbingDataManager.preview)