From d2de5cc89377eacd6c6fc42e0b4af5729af2a877 Mon Sep 17 00:00:00 2001 From: YHC66 Date: Thu, 10 Apr 2025 01:41:15 -0700 Subject: [PATCH 1/9] Gymdetailview rewrite --- berkeley-mobile.xcodeproj/project.pbxproj | 4 + .../Fitness/GymDetailSwiftUIView.swift | 215 ++++++++++++++++++ .../Fitness/GymDetailViewController.swift | 12 +- 3 files changed, 222 insertions(+), 9 deletions(-) create mode 100644 berkeley-mobile/Fitness/GymDetailSwiftUIView.swift diff --git a/berkeley-mobile.xcodeproj/project.pbxproj b/berkeley-mobile.xcodeproj/project.pbxproj index 380b072ce..f7675f5cb 100644 --- a/berkeley-mobile.xcodeproj/project.pbxproj +++ b/berkeley-mobile.xcodeproj/project.pbxproj @@ -81,6 +81,7 @@ 13EA64D02399D50C00FD8E13 /* DataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA64CF2399D50C00FD8E13 /* DataManager.swift */; }; 1DB006AD2D71C8D6001CC870 /* ResourcesSectionDropdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DB006AC2D71C8C0001CC870 /* ResourcesSectionDropdown.swift */; }; 1DB88F6D2D94DF78007713F7 /* OpenTimesCardSwiftUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DB88F6C2D94DF78007713F7 /* OpenTimesCardSwiftUIView.swift */; }; + 1DB88F712DA76AC2007713F7 /* GymDetailSwiftUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DB88F702DA76AC2007713F7 /* GymDetailSwiftUIView.swift */; }; 29061D41241C450E002BC9D9 /* HasLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29061D40241C450E002BC9D9 /* HasLocation.swift */; }; 2913595724B136BE00DE9AD6 /* CollapsibleCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2913595624B136BE00DE9AD6 /* CollapsibleCardView.swift */; }; 2913595924B13DF200DE9AD6 /* OpenTimesCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2913595824B13DF200DE9AD6 /* OpenTimesCardView.swift */; }; @@ -273,6 +274,7 @@ 13EA64CF2399D50C00FD8E13 /* DataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataManager.swift; sourceTree = ""; }; 1DB006AC2D71C8C0001CC870 /* ResourcesSectionDropdown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourcesSectionDropdown.swift; sourceTree = ""; }; 1DB88F6C2D94DF78007713F7 /* OpenTimesCardSwiftUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenTimesCardSwiftUIView.swift; sourceTree = ""; }; + 1DB88F702DA76AC2007713F7 /* GymDetailSwiftUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GymDetailSwiftUIView.swift; sourceTree = ""; }; 29061D40241C450E002BC9D9 /* HasLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HasLocation.swift; sourceTree = ""; }; 2913595624B136BE00DE9AD6 /* CollapsibleCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleCardView.swift; sourceTree = ""; }; 2913595824B13DF200DE9AD6 /* OpenTimesCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenTimesCardView.swift; sourceTree = ""; }; @@ -522,6 +524,7 @@ 13EA64C62399CDF900FD8E13 /* GymDataSource */, 1336A31D241C400F00949F32 /* GymClassDataSource */, 01D11B8F2504560700BDF660 /* GymDetailViewController.swift */, + 1DB88F702DA76AC2007713F7 /* GymDetailSwiftUIView.swift */, 13E25DFC238C949E00B670B5 /* FitnessViewController.swift */, 135D7F78243AA6B1003F8BD1 /* Fitness+Controllers */, E83B6DA42D7A85D500AA9422 /* GymOccupancyScrapper.swift */, @@ -1254,6 +1257,7 @@ 1336A320241D924300949F32 /* DiningItem.swift in Sources */, 299ACFB4244A5F90000F3E86 /* HasOccupancy.swift in Sources */, E8912C2A2D869D6900C645B9 /* BMConstants.swift in Sources */, + 1DB88F712DA76AC2007713F7 /* GymDetailSwiftUIView.swift in Sources */, 2969747625D8A77C005ED231 /* UserDefaultKeys.swift in Sources */, 559A7B6D2373DEFA004EA501 /* MaterialTableViewCell.swift in Sources */, 13EA64CD2399CEDA00FD8E13 /* Gym.swift in Sources */, diff --git a/berkeley-mobile/Fitness/GymDetailSwiftUIView.swift b/berkeley-mobile/Fitness/GymDetailSwiftUIView.swift new file mode 100644 index 000000000..7885e0525 --- /dev/null +++ b/berkeley-mobile/Fitness/GymDetailSwiftUIView.swift @@ -0,0 +1,215 @@ +// +// GymDetailSwiftUIView.swift +// +// Created by Yihang Chen on 4/9/25. +// Copyright © 2025 ASUC OCTO. All rights reserved. +// + +import SwiftUI +import Firebase + +// Remove the invalid type alias +// typealias GymDetailView = GymDetailSwiftUIView + +struct GymDetailSwiftUIView: View { + let gym: Gym + + @Environment(\.openURL) private var openURL + + // Allow initialization with just a gym to match existing GymDetailView interface + init(gym: Gym) { + self.gym = gym + } + + var body: some View { + ScrollView { + VStack(spacing: 16) { + overviewCard + + if gym.weeklyHours != nil { + OpenTimesCardSwiftUIView(item: gym) + } + + if let website = gym.website { + actionButton(title: "Learn More") { + openURL(website) + } + } + + if let description = gym.description, !description.isEmpty { + descriptionCard(description: description) + } + } + .padding(.horizontal, 16) + .padding(.vertical, 5) + } + .navigationTitle(gym.name) + .onAppear { + Analytics.logEvent("opened_gym", parameters: ["gym": gym.name]) + } + } + + // Overview card showing image, name, address, and other basic info + private var overviewCard: some View { + VStack { + HStack(alignment: .top, spacing: 16) { + // Left column with title and details + VStack(alignment: .leading) { + // Title at the top + Text(gym.name) + .font(.system(size: 28, weight: .bold)) + .foregroundColor(Color(BMColor.blackText)) + .lineLimit(3) + .padding(.top, 8) + + Spacer(minLength: 80) + + // Contact info at the bottom + VStack(alignment: .leading, spacing: 12) { + // Address + if let address = gym.address, !address.isEmpty { + HStack(alignment: .top, spacing: 10) { + Image(systemName: "location.fill") + .foregroundColor(.gray) + .frame(width: 16, height: 16) + + Text(address) + .font(.system(size: 11, weight: .regular)) + .foregroundColor(.black) + .lineLimit(2) + } + .padding(.bottom, 1) + } + + // Phone and distance on the same line + HStack(spacing: 4) { + // Phone number + if let phoneNumber = gym.phoneNumber, !phoneNumber.isEmpty { + HStack(spacing: 5) { + Image(systemName: "phone.fill") + .foregroundColor(.gray) + .frame(width: 16, height: 16) + + Text(phoneNumber) + .font(.system(size: 11, weight: .regular)) + .foregroundColor(.black) + .lineLimit(1) + .fixedSize(horizontal: true, vertical: false) + } + } + + Spacer(minLength: 4) + + // Distance + if let hasLocation = gym as? HasLocation, + let distance = hasLocation.distanceToUser { + HStack() { + Image(systemName: "figure.walk") + .foregroundColor(.gray) + .frame(width: 16, height: 16) + + Text(String(format: "%.1f mi", distance)) + .font(.system(size: 11, weight: .regular)) + .foregroundColor(.black) + } + } + } + + } + } + + Spacer() + + // Image on the right + if let imageURL = gym.imageURL { + AsyncImage(url: imageURL) { phase in + switch phase { + case .empty: + Image(systemName: "photo") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 110, height: 220) + .foregroundColor(.gray.opacity(0.3)) + .background(Color.gray.opacity(0.1)) + .clipShape(RoundedRectangle(cornerRadius: 8)) + case .success(let image): + image + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 110, height: 220) + .clipShape(RoundedRectangle(cornerRadius: 8)) + case .failure: + Image(systemName: "photo") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 110, height: 220) + .foregroundColor(.gray) + .background(Color.gray.opacity(0.1)) + .clipShape(RoundedRectangle(cornerRadius: 8)) + @unknown default: + EmptyView() + } + } + } + } + .padding(8) + } + .background(Color(BMColor.cardBackground)) + .cornerRadius(12) + .shadow(color: .black.opacity(0.25), radius: 5, x: 0, y: 0) + } + + + // Custom action button (like "Learn More") + private func actionButton(title: String, action: @escaping () -> Void) -> some View { + Button(action: action) { + Text(title) + .font(.system(size: 16, weight: .semibold)) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .padding(.vertical, 12) + .background(Color.blue) + .cornerRadius(10) + } + } + + // Description card showing the gym description + private func descriptionCard(description: String) -> some View { + VStack(alignment: .leading, spacing: 10) { + Text("Description") + .font(.system(size: 16, weight: .bold)) + .foregroundColor(Color(BMColor.blackText)) + + Text(description) + .font(.system(size: 12, weight: .light)) + .foregroundColor(Color(BMColor.blackText)) + } + .padding(16) + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color(BMColor.cardBackground)) + .cornerRadius(12) + + } +} + +// A SwiftUI preview for the GymDetailView +#Preview { + // Create a sample gym for preview + let sampleGym = Gym( + name: "RSF (Recreational Sports Facility)", + description: "The Recreational Sports Facility (RSF) is UC Berkeley's largest fitness center, offering state-of-the-art equipment, group exercise classes, and various sports courts. Located at the heart of campus, it provides comprehensive fitness options for students and faculty.", + address: "2301 Bancroft Way, Berkeley, CA 94720", + phoneNumber: "(510) 111-2222", + imageLink: "https://firebasestorage.googleapis.com/v0/b/berkeley-mobile.appspot.com/o/images%2FRSF.jpg?alt=media&token=b645d675-6f51-45ea-99f7-9b36576e14b7", + weeklyHours: nil, + link: "https://recsports.berkeley.edu/rsf/" + ) + + // Set a fake distance + sampleGym.latitude = 37.8687 + sampleGym.longitude = -122.2614 + + return NavigationView { + GymDetailSwiftUIView(gym: sampleGym) + } +} diff --git a/berkeley-mobile/Fitness/GymDetailViewController.swift b/berkeley-mobile/Fitness/GymDetailViewController.swift index dd9c3c053..869119a9d 100644 --- a/berkeley-mobile/Fitness/GymDetailViewController.swift +++ b/berkeley-mobile/Fitness/GymDetailViewController.swift @@ -13,22 +13,16 @@ import SwiftUI // MARK: - GymDetailView -struct GymDetailView: UIViewControllerRepresentable { - typealias UIViewControllerType = GymDetailViewController - +struct GymDetailView: View { private let gym: Gym init(gym: Gym) { self.gym = gym } - func makeUIViewController(context: Context) -> GymDetailViewController { - let gymDetailVC = GymDetailViewController() - gymDetailVC.gym = gym - return gymDetailVC + var body: some View { + GymDetailSwiftUIView(gym: gym) } - - func updateUIViewController(_ uiViewController: GymDetailViewController, context: Context) {} } From e7b9476f1a55894ef4d4840f5562214c59741cca Mon Sep 17 00:00:00 2001 From: YHC66 Date: Thu, 10 Apr 2025 12:17:47 -0700 Subject: [PATCH 2/9] Update GymDetailSwiftUIView.swift --- berkeley-mobile/Fitness/GymDetailSwiftUIView.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/berkeley-mobile/Fitness/GymDetailSwiftUIView.swift b/berkeley-mobile/Fitness/GymDetailSwiftUIView.swift index 7885e0525..bcd7713be 100644 --- a/berkeley-mobile/Fitness/GymDetailSwiftUIView.swift +++ b/berkeley-mobile/Fitness/GymDetailSwiftUIView.swift @@ -82,7 +82,7 @@ struct GymDetailSwiftUIView: View { } // Phone and distance on the same line - HStack(spacing: 4) { + HStack(spacing: 10) { // Phone number if let phoneNumber = gym.phoneNumber, !phoneNumber.isEmpty { HStack(spacing: 5) { @@ -98,12 +98,12 @@ struct GymDetailSwiftUIView: View { } } - Spacer(minLength: 4) + // Distance if let hasLocation = gym as? HasLocation, let distance = hasLocation.distanceToUser { - HStack() { + HStack(spacing: 3) { Image(systemName: "figure.walk") .foregroundColor(.gray) .frame(width: 16, height: 16) @@ -128,7 +128,7 @@ struct GymDetailSwiftUIView: View { Image(systemName: "photo") .resizable() .aspectRatio(contentMode: .fit) - .frame(width: 110, height: 220) + .frame(width: 120, height: 220) .foregroundColor(.gray.opacity(0.3)) .background(Color.gray.opacity(0.1)) .clipShape(RoundedRectangle(cornerRadius: 8)) @@ -136,13 +136,13 @@ struct GymDetailSwiftUIView: View { image .resizable() .aspectRatio(contentMode: .fill) - .frame(width: 110, height: 220) + .frame(width: 120, height: 220) .clipShape(RoundedRectangle(cornerRadius: 8)) case .failure: Image(systemName: "photo") .resizable() .aspectRatio(contentMode: .fit) - .frame(width: 110, height: 220) + .frame(width: 120, height: 220) .foregroundColor(.gray) .background(Color.gray.opacity(0.1)) .clipShape(RoundedRectangle(cornerRadius: 8)) From 6116dd0e83f28d8d709d695b9aee4c70bcf24eb8 Mon Sep 17 00:00:00 2001 From: YHC66 Date: Fri, 18 Apr 2025 16:29:09 -0700 Subject: [PATCH 3/9] Update gymdetailview --- .../Fitness/GymDataSource/Gym.swift | 15 ++- .../Fitness/GymDetailSwiftUIView.swift | 103 +++++++++--------- 2 files changed, 64 insertions(+), 54 deletions(-) diff --git a/berkeley-mobile/Fitness/GymDataSource/Gym.swift b/berkeley-mobile/Fitness/GymDataSource/Gym.swift index 7168301c1..85d786f6e 100644 --- a/berkeley-mobile/Fitness/GymDataSource/Gym.swift +++ b/berkeley-mobile/Fitness/GymDataSource/Gym.swift @@ -62,9 +62,20 @@ class Gym: SearchItem, HasLocation, CanFavorite, HasPhoneNumber, HasImage, HasOp self.phoneNumber = phoneNumber self.weeklyHours = weeklyHours self.name = name.trimmingCharacters(in: .whitespacesAndNewlines) - self.imageURL = URL(string: imageLink ?? "") + + if let imgLink = imageLink, !imgLink.isEmpty, let url = URL(string: imgLink) { + self.imageURL = url + } else { + self.imageURL = nil + } + self.icon = UIImage(named: "Walk")?.colored(BMColor.blackText) - self.website = URL(string: link ?? "") + + if let webLink = link, !webLink.isEmpty, let url = URL(string: webLink) { + self.website = url + } else { + self.website = nil + } } } diff --git a/berkeley-mobile/Fitness/GymDetailSwiftUIView.swift b/berkeley-mobile/Fitness/GymDetailSwiftUIView.swift index bcd7713be..02c820f02 100644 --- a/berkeley-mobile/Fitness/GymDetailSwiftUIView.swift +++ b/berkeley-mobile/Fitness/GymDetailSwiftUIView.swift @@ -8,15 +8,10 @@ import SwiftUI import Firebase -// Remove the invalid type alias -// typealias GymDetailView = GymDetailSwiftUIView - struct GymDetailSwiftUIView: View { - let gym: Gym - @Environment(\.openURL) private var openURL - - // Allow initialization with just a gym to match existing GymDetailView interface + + let gym: Gym init(gym: Gym) { self.gym = gym } @@ -49,68 +44,57 @@ struct GymDetailSwiftUIView: View { } } - // Overview card showing image, name, address, and other basic info private var overviewCard: some View { VStack { HStack(alignment: .top, spacing: 16) { - // Left column with title and details VStack(alignment: .leading) { - // Title at the top Text(gym.name) - .font(.system(size: 28, weight: .bold)) + .font(Font(BMFont.bold(28))) .foregroundColor(Color(BMColor.blackText)) .lineLimit(3) .padding(.top, 8) Spacer(minLength: 80) - // Contact info at the bottom VStack(alignment: .leading, spacing: 12) { - // Address if let address = gym.address, !address.isEmpty { HStack(alignment: .top, spacing: 10) { Image(systemName: "location.fill") - .foregroundColor(.gray) + .foregroundColor(Color(BMColor.blackText)) .frame(width: 16, height: 16) Text(address) - .font(.system(size: 11, weight: .regular)) - .foregroundColor(.black) + .font(Font(BMFont.light(11))) + .foregroundColor(Color(BMColor.blackText)) .lineLimit(2) } .padding(.bottom, 1) } - // Phone and distance on the same line HStack(spacing: 10) { - // Phone number if let phoneNumber = gym.phoneNumber, !phoneNumber.isEmpty { HStack(spacing: 5) { Image(systemName: "phone.fill") - .foregroundColor(.gray) + .foregroundColor(Color(BMColor.blackText)) .frame(width: 16, height: 16) Text(phoneNumber) - .font(.system(size: 11, weight: .regular)) - .foregroundColor(.black) + .font(Font(BMFont.light(11))) + .foregroundColor(Color(BMColor.blackText)) .lineLimit(1) .fixedSize(horizontal: true, vertical: false) } } - - - - // Distance if let hasLocation = gym as? HasLocation, let distance = hasLocation.distanceToUser { HStack(spacing: 3) { Image(systemName: "figure.walk") - .foregroundColor(.gray) + .foregroundColor(Color(BMColor.blackText)) .frame(width: 16, height: 16) Text(String(format: "%.1f mi", distance)) - .font(.system(size: 11, weight: .regular)) - .foregroundColor(.black) + .font(Font(BMFont.light(11))) + .foregroundColor(Color(BMColor.blackText)) } } } @@ -120,68 +104,84 @@ struct GymDetailSwiftUIView: View { Spacer() - // Image on the right if let imageURL = gym.imageURL { AsyncImage(url: imageURL) { phase in switch phase { case .empty: - Image(systemName: "photo") + Image("DoeGlade") .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 120, height: 220) - .foregroundColor(.gray.opacity(0.3)) - .background(Color.gray.opacity(0.1)) + .aspectRatio(contentMode: .fill) + .frame(width: 120, height: 200) .clipShape(RoundedRectangle(cornerRadius: 8)) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color(uiColor: .systemGray4), lineWidth: 0.5) + ) case .success(let image): image .resizable() .aspectRatio(contentMode: .fill) - .frame(width: 120, height: 220) + .frame(width: 120, height: 200) .clipShape(RoundedRectangle(cornerRadius: 8)) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color(uiColor: .systemGray4), lineWidth: 0.5) + ) case .failure: - Image(systemName: "photo") + // Default image on failure + Image("DoeGlade") .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 120, height: 220) - .foregroundColor(.gray) - .background(Color.gray.opacity(0.1)) + .aspectRatio(contentMode: .fill) + .frame(width: 120, height: 200) .clipShape(RoundedRectangle(cornerRadius: 8)) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color(uiColor: .systemGray4), lineWidth: 0.5) + ) @unknown default: EmptyView() } } + } else { + // Default image when no image URL is available + Image("DoeGlade") + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 120, height: 200) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color(uiColor: .systemGray4), lineWidth: 0.5) + ) } } .padding(8) } .background(Color(BMColor.cardBackground)) .cornerRadius(12) - .shadow(color: .black.opacity(0.25), radius: 5, x: 0, y: 0) + .shadow(color: Color(uiColor: .label).opacity(0.15), radius: 5, x: 0, y: 0) } - - // Custom action button (like "Learn More") private func actionButton(title: String, action: @escaping () -> Void) -> some View { Button(action: action) { Text(title) - .font(.system(size: 16, weight: .semibold)) + .font(Font(BMFont.medium(16))) .foregroundColor(.white) .frame(maxWidth: .infinity) .padding(.vertical, 12) - .background(Color.blue) + .background(Color(BMColor.ActionButton.background)) .cornerRadius(10) } } - // Description card showing the gym description private func descriptionCard(description: String) -> some View { VStack(alignment: .leading, spacing: 10) { Text("Description") - .font(.system(size: 16, weight: .bold)) + .font(Font(BMFont.bold(16))) .foregroundColor(Color(BMColor.blackText)) Text(description) - .font(.system(size: 12, weight: .light)) + .font(Font(BMFont.light(12))) .foregroundColor(Color(BMColor.blackText)) } .padding(16) @@ -192,20 +192,19 @@ struct GymDetailSwiftUIView: View { } } -// A SwiftUI preview for the GymDetailView #Preview { - // Create a sample gym for preview + // Sample gym for preview let sampleGym = Gym( name: "RSF (Recreational Sports Facility)", description: "The Recreational Sports Facility (RSF) is UC Berkeley's largest fitness center, offering state-of-the-art equipment, group exercise classes, and various sports courts. Located at the heart of campus, it provides comprehensive fitness options for students and faculty.", address: "2301 Bancroft Way, Berkeley, CA 94720", phoneNumber: "(510) 111-2222", - imageLink: "https://firebasestorage.googleapis.com/v0/b/berkeley-mobile.appspot.com/o/images%2FRSF.jpg?alt=media&token=b645d675-6f51-45ea-99f7-9b36576e14b7", + imageLink: nil, weeklyHours: nil, link: "https://recsports.berkeley.edu/rsf/" ) - // Set a fake distance + // Randomly generated latitude and longitude sampleGym.latitude = 37.8687 sampleGym.longitude = -122.2614 From 9a444fe72080e41e6869d9dda847d882604e8a27 Mon Sep 17 00:00:00 2001 From: YHC66 Date: Wed, 30 Apr 2025 16:24:21 -0700 Subject: [PATCH 4/9] Refactor components --- .../Fitness/GymDetailSwiftUIView.swift | 221 +++++++++--------- 1 file changed, 108 insertions(+), 113 deletions(-) diff --git a/berkeley-mobile/Fitness/GymDetailSwiftUIView.swift b/berkeley-mobile/Fitness/GymDetailSwiftUIView.swift index 02c820f02..c775f1370 100644 --- a/berkeley-mobile/Fitness/GymDetailSwiftUIView.swift +++ b/berkeley-mobile/Fitness/GymDetailSwiftUIView.swift @@ -26,7 +26,7 @@ struct GymDetailSwiftUIView: View { } if let website = gym.website { - actionButton(title: "Learn More") { + BMActionButton(title: "Learn More") { openURL(website) } } @@ -48,112 +48,16 @@ struct GymDetailSwiftUIView: View { VStack { HStack(alignment: .top, spacing: 16) { VStack(alignment: .leading) { - Text(gym.name) - .font(Font(BMFont.bold(28))) - .foregroundColor(Color(BMColor.blackText)) - .lineLimit(3) - .padding(.top, 8) + GymTitleView(name: gym.name) - Spacer(minLength: 80) + Spacer(minLength: 20) - VStack(alignment: .leading, spacing: 12) { - if let address = gym.address, !address.isEmpty { - HStack(alignment: .top, spacing: 10) { - Image(systemName: "location.fill") - .foregroundColor(Color(BMColor.blackText)) - .frame(width: 16, height: 16) - - Text(address) - .font(Font(BMFont.light(11))) - .foregroundColor(Color(BMColor.blackText)) - .lineLimit(2) - } - .padding(.bottom, 1) - } - - HStack(spacing: 10) { - if let phoneNumber = gym.phoneNumber, !phoneNumber.isEmpty { - HStack(spacing: 5) { - Image(systemName: "phone.fill") - .foregroundColor(Color(BMColor.blackText)) - .frame(width: 16, height: 16) - - Text(phoneNumber) - .font(Font(BMFont.light(11))) - .foregroundColor(Color(BMColor.blackText)) - .lineLimit(1) - .fixedSize(horizontal: true, vertical: false) - } - } - if let hasLocation = gym as? HasLocation, - let distance = hasLocation.distanceToUser { - HStack(spacing: 3) { - Image(systemName: "figure.walk") - .foregroundColor(Color(BMColor.blackText)) - .frame(width: 16, height: 16) - - Text(String(format: "%.1f mi", distance)) - .font(Font(BMFont.light(11))) - .foregroundColor(Color(BMColor.blackText)) - } - } - } - - } + GymContactInfoView(gym: gym) } Spacer() - if let imageURL = gym.imageURL { - AsyncImage(url: imageURL) { phase in - switch phase { - case .empty: - Image("DoeGlade") - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 120, height: 200) - .clipShape(RoundedRectangle(cornerRadius: 8)) - .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(Color(uiColor: .systemGray4), lineWidth: 0.5) - ) - case .success(let image): - image - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 120, height: 200) - .clipShape(RoundedRectangle(cornerRadius: 8)) - .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(Color(uiColor: .systemGray4), lineWidth: 0.5) - ) - case .failure: - // Default image on failure - Image("DoeGlade") - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 120, height: 200) - .clipShape(RoundedRectangle(cornerRadius: 8)) - .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(Color(uiColor: .systemGray4), lineWidth: 0.5) - ) - @unknown default: - EmptyView() - } - } - } else { - // Default image when no image URL is available - Image("DoeGlade") - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 120, height: 200) - .clipShape(RoundedRectangle(cornerRadius: 8)) - .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(Color(uiColor: .systemGray4), lineWidth: 0.5) - ) - } + GymImageView(imageURL: gym.imageURL) } .padding(8) } @@ -162,15 +66,109 @@ struct GymDetailSwiftUIView: View { .shadow(color: Color(uiColor: .label).opacity(0.15), radius: 5, x: 0, y: 0) } - private func actionButton(title: String, action: @escaping () -> Void) -> some View { - Button(action: action) { - Text(title) - .font(Font(BMFont.medium(16))) - .foregroundColor(.white) - .frame(maxWidth: .infinity) - .padding(.vertical, 12) - .background(Color(BMColor.ActionButton.background)) - .cornerRadius(10) + private struct GymTitleView: View { + let name: String + + var body: some View { + Text(name) + .font(Font(BMFont.bold(28))) + .foregroundColor(Color(BMColor.blackText)) + .lineLimit(3) + .padding(.top, 8) + } + } + + private struct GymContactInfoView: View { + let gym: Gym + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + if let address = gym.address, !address.isEmpty { + ContactInfoRow( + iconName: "location.fill", + text: address, + lineLimit: nil + ) + } + + if let phoneNumber = gym.phoneNumber, !phoneNumber.isEmpty { + ContactInfoRow( + iconName: "phone.fill", + text: phoneNumber + ) + } + + if let hasLocation = gym as? HasLocation, + let distance = hasLocation.distanceToUser { + ContactInfoRow( + iconName: "figure.walk", + text: String(format: "%.1f miles", distance) + ) + } + } + } + } + + private struct ContactInfoRow: View { + let iconName: String + let text: String + var lineLimit: Int? = 1 + + var body: some View { + HStack(alignment: .top, spacing: 10) { + Image(systemName: iconName) + .foregroundColor(Color(BMColor.blackText)) + .frame(width: 18, height: 18) + + Text(text) + .font(Font(BMFont.light(14))) + .foregroundColor(Color(BMColor.blackText)) + .lineLimit(lineLimit) + .fixedSize(horizontal: false, vertical: true) + } + } + } + + private struct GymImageView: View { + let imageURL: URL? + + var body: some View { + if let imageURL = imageURL { + AsyncImage(url: imageURL) { phase in + switch phase { + case .empty: + defaultImage + case .success(let image): + image + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 120, height: 200) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color(uiColor: .systemGray4), lineWidth: 0.5) + ) + case .failure: + defaultImage + @unknown default: + EmptyView() + } + } + } else { + defaultImage + } + } + + private var defaultImage: some View { + Image(Constants.doeGladeImage) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 120, height: 200) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color(uiColor: .systemGray4), lineWidth: 0.5) + ) } } @@ -188,12 +186,10 @@ struct GymDetailSwiftUIView: View { .frame(maxWidth: .infinity, alignment: .leading) .background(Color(BMColor.cardBackground)) .cornerRadius(12) - } } #Preview { - // Sample gym for preview let sampleGym = Gym( name: "RSF (Recreational Sports Facility)", description: "The Recreational Sports Facility (RSF) is UC Berkeley's largest fitness center, offering state-of-the-art equipment, group exercise classes, and various sports courts. Located at the heart of campus, it provides comprehensive fitness options for students and faculty.", @@ -204,7 +200,6 @@ struct GymDetailSwiftUIView: View { link: "https://recsports.berkeley.edu/rsf/" ) - // Randomly generated latitude and longitude sampleGym.latitude = 37.8687 sampleGym.longitude = -122.2614 From 7dfa2109447eaae0f2aea66fd48c6d895f675cd5 Mon Sep 17 00:00:00 2001 From: YHC66 Date: Wed, 30 Apr 2025 16:25:34 -0700 Subject: [PATCH 5/9] fix Constant.doegladeimage --- berkeley-mobile/Fitness/GymDetailSwiftUIView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/berkeley-mobile/Fitness/GymDetailSwiftUIView.swift b/berkeley-mobile/Fitness/GymDetailSwiftUIView.swift index c775f1370..a2d949783 100644 --- a/berkeley-mobile/Fitness/GymDetailSwiftUIView.swift +++ b/berkeley-mobile/Fitness/GymDetailSwiftUIView.swift @@ -160,7 +160,7 @@ struct GymDetailSwiftUIView: View { } private var defaultImage: some View { - Image(Constants.doeGladeImage) + Image(uiImage: Constants.doeGladeImage) .resizable() .aspectRatio(contentMode: .fill) .frame(width: 120, height: 200) From a1e9e400cbf62e5666123e16414c340f090e2c95 Mon Sep 17 00:00:00 2001 From: YHC66 Date: Wed, 30 Apr 2025 17:37:56 -0700 Subject: [PATCH 6/9] Delete and Rename file --- berkeley-mobile.xcodeproj/project.pbxproj | 12 +- ...lSwiftUIView.swift => GymDetailView.swift} | 6 +- .../Fitness/GymDetailViewController.swift | 127 ------------------ 3 files changed, 7 insertions(+), 138 deletions(-) rename berkeley-mobile/Fitness/{GymDetailSwiftUIView.swift => GymDetailView.swift} (98%) delete mode 100644 berkeley-mobile/Fitness/GymDetailViewController.swift diff --git a/berkeley-mobile.xcodeproj/project.pbxproj b/berkeley-mobile.xcodeproj/project.pbxproj index f7675f5cb..4a9cdede8 100644 --- a/berkeley-mobile.xcodeproj/project.pbxproj +++ b/berkeley-mobile.xcodeproj/project.pbxproj @@ -27,7 +27,6 @@ 01CDBBF125CA6F58006B93BD /* RequestError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01CDBBF025CA6F58006B93BD /* RequestError.swift */; }; 01CDFF6A257C614900D9FBD6 /* Colors+Resource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01CDFF69257C614900D9FBD6 /* Colors+Resource.swift */; }; 01D11B8E2504453B00BDF660 /* ScrollingStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D11B8D2504453B00BDF660 /* ScrollingStackView.swift */; }; - 01D11B902504560700BDF660 /* GymDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D11B8F2504560700BDF660 /* GymDetailViewController.swift */; }; 01D269932544D86C000377B4 /* Apercu Light.otf in Resources */ = {isa = PBXBuildFile; fileRef = 01D2698A2544D86B000377B4 /* Apercu Light.otf */; }; 01D269942544D86C000377B4 /* Apercu Italic.otf in Resources */ = {isa = PBXBuildFile; fileRef = 01D2698B2544D86B000377B4 /* Apercu Italic.otf */; }; 01D269952544D86C000377B4 /* Apercu Light Italic.otf in Resources */ = {isa = PBXBuildFile; fileRef = 01D2698C2544D86B000377B4 /* Apercu Light Italic.otf */; }; @@ -79,9 +78,9 @@ 13EA64CA2399CE5B00FD8E13 /* SearchItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA64C92399CE5B00FD8E13 /* SearchItem.swift */; }; 13EA64CD2399CEDA00FD8E13 /* Gym.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA64CC2399CEDA00FD8E13 /* Gym.swift */; }; 13EA64D02399D50C00FD8E13 /* DataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA64CF2399D50C00FD8E13 /* DataManager.swift */; }; + 1D7DD6E02DC2EB5B00A6BBA7 /* GymDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D7DD6DF2DC2EB5B00A6BBA7 /* GymDetailView.swift */; }; 1DB006AD2D71C8D6001CC870 /* ResourcesSectionDropdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DB006AC2D71C8C0001CC870 /* ResourcesSectionDropdown.swift */; }; 1DB88F6D2D94DF78007713F7 /* OpenTimesCardSwiftUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DB88F6C2D94DF78007713F7 /* OpenTimesCardSwiftUIView.swift */; }; - 1DB88F712DA76AC2007713F7 /* GymDetailSwiftUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DB88F702DA76AC2007713F7 /* GymDetailSwiftUIView.swift */; }; 29061D41241C450E002BC9D9 /* HasLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29061D40241C450E002BC9D9 /* HasLocation.swift */; }; 2913595724B136BE00DE9AD6 /* CollapsibleCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2913595624B136BE00DE9AD6 /* CollapsibleCardView.swift */; }; 2913595924B13DF200DE9AD6 /* OpenTimesCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2913595824B13DF200DE9AD6 /* OpenTimesCardView.swift */; }; @@ -218,7 +217,6 @@ 01CDBBF025CA6F58006B93BD /* RequestError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestError.swift; sourceTree = ""; }; 01CDFF69257C614900D9FBD6 /* Colors+Resource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Colors+Resource.swift"; sourceTree = ""; }; 01D11B8D2504453B00BDF660 /* ScrollingStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollingStackView.swift; sourceTree = ""; }; - 01D11B8F2504560700BDF660 /* GymDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GymDetailViewController.swift; sourceTree = ""; }; 01D2698A2544D86B000377B4 /* Apercu Light.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Apercu Light.otf"; sourceTree = ""; }; 01D2698B2544D86B000377B4 /* Apercu Italic.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Apercu Italic.otf"; sourceTree = ""; }; 01D2698C2544D86B000377B4 /* Apercu Light Italic.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Apercu Light Italic.otf"; sourceTree = ""; }; @@ -272,9 +270,9 @@ 13EA64C92399CE5B00FD8E13 /* SearchItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchItem.swift; sourceTree = ""; }; 13EA64CC2399CEDA00FD8E13 /* Gym.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Gym.swift; sourceTree = ""; }; 13EA64CF2399D50C00FD8E13 /* DataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataManager.swift; sourceTree = ""; }; + 1D7DD6DF2DC2EB5B00A6BBA7 /* GymDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GymDetailView.swift; sourceTree = ""; }; 1DB006AC2D71C8C0001CC870 /* ResourcesSectionDropdown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourcesSectionDropdown.swift; sourceTree = ""; }; 1DB88F6C2D94DF78007713F7 /* OpenTimesCardSwiftUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenTimesCardSwiftUIView.swift; sourceTree = ""; }; - 1DB88F702DA76AC2007713F7 /* GymDetailSwiftUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GymDetailSwiftUIView.swift; sourceTree = ""; }; 29061D40241C450E002BC9D9 /* HasLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HasLocation.swift; sourceTree = ""; }; 2913595624B136BE00DE9AD6 /* CollapsibleCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleCardView.swift; sourceTree = ""; }; 2913595824B13DF200DE9AD6 /* OpenTimesCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenTimesCardView.swift; sourceTree = ""; }; @@ -523,13 +521,12 @@ children = ( 13EA64C62399CDF900FD8E13 /* GymDataSource */, 1336A31D241C400F00949F32 /* GymClassDataSource */, - 01D11B8F2504560700BDF660 /* GymDetailViewController.swift */, - 1DB88F702DA76AC2007713F7 /* GymDetailSwiftUIView.swift */, 13E25DFC238C949E00B670B5 /* FitnessViewController.swift */, 135D7F78243AA6B1003F8BD1 /* Fitness+Controllers */, E83B6DA42D7A85D500AA9422 /* GymOccupancyScrapper.swift */, E83B6DA62D7A85F200AA9422 /* GymOccupancyView.swift */, E83B6DA82D7A860E00AA9422 /* GymOccupancyViewModel.swift */, + 1D7DD6DF2DC2EB5B00A6BBA7 /* GymDetailView.swift */, ); path = Fitness; sourceTree = ""; @@ -1190,6 +1187,7 @@ 01D2699D2544E005000377B4 /* AcademicCalendarViewController.swift in Sources */, 1336A31C241C400800949F32 /* GymClass.swift in Sources */, E8B5975F2CA62D6F006DFBD5 /* SegmentedControlView.swift in Sources */, + 1D7DD6E02DC2EB5B00A6BBA7 /* GymDetailView.swift in Sources */, 13EA64C82399CE0800FD8E13 /* GymDataSource.swift in Sources */, 2E1C227D2D835A9D0021803C /* SearchBarView.swift in Sources */, 298EE26A25BB6C33002BAF0F /* Colors+StudyPact.swift in Sources */, @@ -1238,7 +1236,6 @@ 017C0B26251018BA00BFA80A /* Colors+MapMarker.swift in Sources */, E83B6DA92D7A860E00AA9422 /* GymOccupancyViewModel.swift in Sources */, 29345E2724A7E76300859A88 /* OverviewCardView.swift in Sources */, - 01D11B902504560700BDF660 /* GymDetailViewController.swift in Sources */, 55AF442D2453ACE600F13232 /* DiningLocation.swift in Sources */, 1336A329241DA56100949F32 /* DiningHallDataSource.swift in Sources */, 136DC97B2398B4D1009B1810 /* UIViewController+Extensions.swift in Sources */, @@ -1257,7 +1254,6 @@ 1336A320241D924300949F32 /* DiningItem.swift in Sources */, 299ACFB4244A5F90000F3E86 /* HasOccupancy.swift in Sources */, E8912C2A2D869D6900C645B9 /* BMConstants.swift in Sources */, - 1DB88F712DA76AC2007713F7 /* GymDetailSwiftUIView.swift in Sources */, 2969747625D8A77C005ED231 /* UserDefaultKeys.swift in Sources */, 559A7B6D2373DEFA004EA501 /* MaterialTableViewCell.swift in Sources */, 13EA64CD2399CEDA00FD8E13 /* Gym.swift in Sources */, diff --git a/berkeley-mobile/Fitness/GymDetailSwiftUIView.swift b/berkeley-mobile/Fitness/GymDetailView.swift similarity index 98% rename from berkeley-mobile/Fitness/GymDetailSwiftUIView.swift rename to berkeley-mobile/Fitness/GymDetailView.swift index a2d949783..552d17203 100644 --- a/berkeley-mobile/Fitness/GymDetailSwiftUIView.swift +++ b/berkeley-mobile/Fitness/GymDetailView.swift @@ -1,5 +1,5 @@ // -// GymDetailSwiftUIView.swift +// GymDetailView.swift // // Created by Yihang Chen on 4/9/25. // Copyright © 2025 ASUC OCTO. All rights reserved. @@ -8,7 +8,7 @@ import SwiftUI import Firebase -struct GymDetailSwiftUIView: View { +struct GymDetailView: View { @Environment(\.openURL) private var openURL let gym: Gym @@ -204,6 +204,6 @@ struct GymDetailSwiftUIView: View { sampleGym.longitude = -122.2614 return NavigationView { - GymDetailSwiftUIView(gym: sampleGym) + GymDetailView(gym: sampleGym) } } diff --git a/berkeley-mobile/Fitness/GymDetailViewController.swift b/berkeley-mobile/Fitness/GymDetailViewController.swift deleted file mode 100644 index 869119a9d..000000000 --- a/berkeley-mobile/Fitness/GymDetailViewController.swift +++ /dev/null @@ -1,127 +0,0 @@ -// -// GymDetailViewController.swift -// bm-persona -// -// Created by Kevin Hu on 9/5/20. -// Copyright © 2020 RJ Pimentel. All rights reserved. -// - -import Firebase -import UIKit -import SwiftUI - - -// MARK: - GymDetailView - -struct GymDetailView: View { - private let gym: Gym - - init(gym: Gym) { - self.gym = gym - } - - var body: some View { - GymDetailSwiftUIView(gym: gym) - } -} - - -// MARK: - GymDetailViewController - -fileprivate let kViewMargin: CGFloat = 16 - -class GymDetailViewController: UIViewController { - - var gym: Gym! - - var overviewCard: OverviewCardView! - var openTimesCard: OpenTimesCardView? - var occupancyCard: OccupancyGraphCardView? - var moreButton: ActionButton? - var descriptionCard: DescriptionCardView? - - override func viewDidLoad() { - super.viewDidLoad() - - setUpScrollView() - setUpOverviewCard() - setUpOpenTimesCard() - setUpoccupancyCard() - setupMoreButton() - setupDescriptionCard() - } - - /// Opens `gym.website` in Safari. Called as a result of tapping on `moreButton`. - @objc private func moreButtonClicked(sender: UIButton) { - guard let url = gym.website else { return } - UIApplication.shared.open(url, options: [:]) - } - - var scrollingStackView: ScrollingStackView = { - let scrollingStackView = ScrollingStackView() - scrollingStackView.scrollView.showsVerticalScrollIndicator = false - scrollingStackView.stackView.spacing = kViewMargin - return scrollingStackView - }() -} - -// MARK: - View - -extension GymDetailViewController { - - func setUpOverviewCard() { - overviewCard = OverviewCardView(item: gym, excludedElements: [.openTimes, .occupancy]) - overviewCard.heightAnchor.constraint(equalToConstant: 200).isActive = true - scrollingStackView.stackView.addArrangedSubview(overviewCard) - } - - func setUpOpenTimesCard() { - guard gym.weeklyHours != nil else { return } - openTimesCard = OpenTimesCardView(item: gym, animationView: scrollingStackView, toggleAction: { _ in - }) - guard let openTimesCard = self.openTimesCard else { return } - scrollingStackView.stackView.addArrangedSubview(openTimesCard) - } - - func setUpoccupancyCard() { - guard let occupancy = gym.occupancy, let forDay = occupancy.occupancy(for: DayOfWeek.weekday(Date())), forDay.count > 0 else { return } - occupancyCard = OccupancyGraphCardView(occupancy: occupancy, isOpen: gym.isOpen) - guard let occupancyCard = self.occupancyCard else { return } - scrollingStackView.stackView.addArrangedSubview(occupancyCard) - } - - - - func setupMoreButton() { - guard gym.website != nil else { return } - let button = ActionButton(title: "Learn More") - button.addTarget(self, action: #selector(moreButtonClicked), for: .touchUpInside) - scrollingStackView.stackView.addArrangedSubview(button) - moreButton = button - } - - func setupDescriptionCard() { - descriptionCard = DescriptionCardView(description: gym.description) - guard let descriptionCard = descriptionCard else { return } - scrollingStackView.stackView.addArrangedSubview(descriptionCard) - } - - func setUpScrollView() { - scrollingStackView.scrollView.setupDummyGesture() - view.addSubview(scrollingStackView) - scrollingStackView.setLayoutMargins(UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 16)) - scrollingStackView.topAnchor.constraint(equalTo: view.topAnchor, constant: kViewMargin).isActive = true - scrollingStackView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true - scrollingStackView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true - scrollingStackView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true - } -} - -// MARK: - Analytics - -extension GymDetailViewController { - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - Analytics.logEvent("opened_gym", parameters: ["gym" : gym.name]) - } -} From d6528ca7176bfda47e6300644a42e5c2d29c09ce Mon Sep 17 00:00:00 2001 From: YHC66 Date: Wed, 30 Apr 2025 18:51:00 -0700 Subject: [PATCH 7/9] Update UI and Refactor overview --- berkeley-mobile/Fitness/GymDetailView.swift | 37 +++++++++++++-------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/berkeley-mobile/Fitness/GymDetailView.swift b/berkeley-mobile/Fitness/GymDetailView.swift index 552d17203..265ba23ca 100644 --- a/berkeley-mobile/Fitness/GymDetailView.swift +++ b/berkeley-mobile/Fitness/GymDetailView.swift @@ -48,42 +48,44 @@ struct GymDetailView: View { VStack { HStack(alignment: .top, spacing: 16) { VStack(alignment: .leading) { - GymTitleView(name: gym.name) + CategoryTitleView(name: gym.name) Spacer(minLength: 20) - GymContactInfoView(gym: gym) + CategoryContactInfoView(category: gym) } Spacer() - GymImageView(imageURL: gym.imageURL) + CategoryImageView(imageURL: gym.imageURL) } - .padding(8) + .padding(12) } .background(Color(BMColor.cardBackground)) .cornerRadius(12) .shadow(color: Color(uiColor: .label).opacity(0.15), radius: 5, x: 0, y: 0) + .padding(.vertical, 8) + .padding(.horizontal, 4) } - private struct GymTitleView: View { + private struct CategoryTitleView: View { let name: String var body: some View { Text(name) - .font(Font(BMFont.bold(28))) + .font(Font(BMFont.bold(23))) .foregroundColor(Color(BMColor.blackText)) .lineLimit(3) .padding(.top, 8) } } - private struct GymContactInfoView: View { - let gym: Gym + private struct CategoryContactInfoView: View { + let category: SearchItem & HasLocation var body: some View { VStack(alignment: .leading, spacing: 12) { - if let address = gym.address, !address.isEmpty { + if let address = category.address, !address.isEmpty { ContactInfoRow( iconName: "location.fill", text: address, @@ -91,15 +93,16 @@ struct GymDetailView: View { ) } - if let phoneNumber = gym.phoneNumber, !phoneNumber.isEmpty { + if let hasContact = category as? HasContact, + let phoneNumber = hasContact.phoneNumber, + !phoneNumber.isEmpty { ContactInfoRow( iconName: "phone.fill", text: phoneNumber ) } - if let hasLocation = gym as? HasLocation, - let distance = hasLocation.distanceToUser { + if let distance = category.distanceToUser { ContactInfoRow( iconName: "figure.walk", text: String(format: "%.1f miles", distance) @@ -121,7 +124,7 @@ struct GymDetailView: View { .frame(width: 18, height: 18) Text(text) - .font(Font(BMFont.light(14))) + .font(Font(BMFont.light(12))) .foregroundColor(Color(BMColor.blackText)) .lineLimit(lineLimit) .fixedSize(horizontal: false, vertical: true) @@ -129,7 +132,7 @@ struct GymDetailView: View { } } - private struct GymImageView: View { + private struct CategoryImageView: View { let imageURL: URL? var body: some View { @@ -189,6 +192,12 @@ struct GymDetailView: View { } } +protocol HasContact { + var phoneNumber: String? { get } +} + +extension Gym: HasContact {} + #Preview { let sampleGym = Gym( name: "RSF (Recreational Sports Facility)", From 52bcdf9be1508e8ad17a7cf1e6b7f857bdcf35ab Mon Sep 17 00:00:00 2001 From: YHC66 Date: Thu, 1 May 2025 11:41:37 -0700 Subject: [PATCH 8/9] Refactor overview card; minor fixes --- berkeley-mobile/Fitness/GymDetailView.swift | 115 +++++++++----------- 1 file changed, 53 insertions(+), 62 deletions(-) diff --git a/berkeley-mobile/Fitness/GymDetailView.swift b/berkeley-mobile/Fitness/GymDetailView.swift index 265ba23ca..aed7e6f80 100644 --- a/berkeley-mobile/Fitness/GymDetailView.swift +++ b/berkeley-mobile/Fitness/GymDetailView.swift @@ -6,61 +6,25 @@ // import SwiftUI -import Firebase -struct GymDetailView: View { - @Environment(\.openURL) private var openURL - - let gym: Gym - init(gym: Gym) { - self.gym = gym - } +struct CategoryOverviewCard: View { + let category: SearchItem & HasLocation & HasImage var body: some View { - ScrollView { - VStack(spacing: 16) { - overviewCard + HStack(alignment: .top, spacing: 16) { + VStack(alignment: .leading) { + TitleView(name: category.name) - if gym.weeklyHours != nil { - OpenTimesCardSwiftUIView(item: gym) - } - - if let website = gym.website { - BMActionButton(title: "Learn More") { - openURL(website) - } - } + Spacer(minLength: 20) - if let description = gym.description, !description.isEmpty { - descriptionCard(description: description) - } + ContactInfoView(category: category) } - .padding(.horizontal, 16) - .padding(.vertical, 5) - } - .navigationTitle(gym.name) - .onAppear { - Analytics.logEvent("opened_gym", parameters: ["gym": gym.name]) - } - } - - private var overviewCard: some View { - VStack { - HStack(alignment: .top, spacing: 16) { - VStack(alignment: .leading) { - CategoryTitleView(name: gym.name) - - Spacer(minLength: 20) - - CategoryContactInfoView(category: gym) - } - - Spacer() - - CategoryImageView(imageURL: gym.imageURL) - } - .padding(12) + + Spacer() + + ImageView(imageURL: category.imageURL) } + .padding(12) .background(Color(BMColor.cardBackground)) .cornerRadius(12) .shadow(color: Color(uiColor: .label).opacity(0.15), radius: 5, x: 0, y: 0) @@ -68,7 +32,7 @@ struct GymDetailView: View { .padding(.horizontal, 4) } - private struct CategoryTitleView: View { + struct TitleView: View { let name: String var body: some View { @@ -80,7 +44,7 @@ struct GymDetailView: View { } } - private struct CategoryContactInfoView: View { + struct ContactInfoView: View { let category: SearchItem & HasLocation var body: some View { @@ -93,8 +57,8 @@ struct GymDetailView: View { ) } - if let hasContact = category as? HasContact, - let phoneNumber = hasContact.phoneNumber, + if let hasPhone = category as? HasPhoneNumber, + let phoneNumber = hasPhone.phoneNumber, !phoneNumber.isEmpty { ContactInfoRow( iconName: "phone.fill", @@ -109,10 +73,11 @@ struct GymDetailView: View { ) } } + .foregroundColor(Color(BMColor.blackText)) } } - private struct ContactInfoRow: View { + struct ContactInfoRow: View { let iconName: String let text: String var lineLimit: Int? = 1 @@ -120,19 +85,17 @@ struct GymDetailView: View { var body: some View { HStack(alignment: .top, spacing: 10) { Image(systemName: iconName) - .foregroundColor(Color(BMColor.blackText)) .frame(width: 18, height: 18) Text(text) .font(Font(BMFont.light(12))) - .foregroundColor(Color(BMColor.blackText)) .lineLimit(lineLimit) .fixedSize(horizontal: false, vertical: true) } } } - private struct CategoryImageView: View { + struct ImageView: View { let imageURL: URL? var body: some View { @@ -174,6 +137,40 @@ struct GymDetailView: View { ) } } +} + +struct GymDetailView: View { + @Environment(\.openURL) private var openURL + + let gym: Gym + init(gym: Gym) { + self.gym = gym + } + + var body: some View { + ScrollView { + VStack(spacing: 16) { + CategoryOverviewCard(category: gym) + + if gym.weeklyHours != nil { + OpenTimesCardSwiftUIView(item: gym) + } + + if let website = gym.website { + BMActionButton(title: "Learn More") { + openURL(website) + } + } + + if let description = gym.description, !description.isEmpty { + descriptionCard(description: description) + } + } + .padding(.horizontal, 16) + .padding(.vertical, 5) + } + .navigationTitle(gym.name) + } private func descriptionCard(description: String) -> some View { VStack(alignment: .leading, spacing: 10) { @@ -192,12 +189,6 @@ struct GymDetailView: View { } } -protocol HasContact { - var phoneNumber: String? { get } -} - -extension Gym: HasContact {} - #Preview { let sampleGym = Gym( name: "RSF (Recreational Sports Facility)", From ce3e28f2f80d9180b1a3da62b19bd925f124ac6d Mon Sep 17 00:00:00 2001 From: YHC66 Date: Fri, 2 May 2025 15:58:26 -0700 Subject: [PATCH 9/9] Create separate files for views --- berkeley-mobile.xcodeproj/project.pbxproj | 8 + .../Common/CategoryOverviewCard.swift | 131 ++++++++++++++ berkeley-mobile/Common/DescriptionCard.swift | 29 +++ berkeley-mobile/Fitness/GymDetailView.swift | 166 ++++++++---------- 4 files changed, 237 insertions(+), 97 deletions(-) create mode 100644 berkeley-mobile/Common/CategoryOverviewCard.swift create mode 100644 berkeley-mobile/Common/DescriptionCard.swift diff --git a/berkeley-mobile.xcodeproj/project.pbxproj b/berkeley-mobile.xcodeproj/project.pbxproj index 4a9cdede8..cb859ba07 100644 --- a/berkeley-mobile.xcodeproj/project.pbxproj +++ b/berkeley-mobile.xcodeproj/project.pbxproj @@ -78,6 +78,8 @@ 13EA64CA2399CE5B00FD8E13 /* SearchItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA64C92399CE5B00FD8E13 /* SearchItem.swift */; }; 13EA64CD2399CEDA00FD8E13 /* Gym.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA64CC2399CEDA00FD8E13 /* Gym.swift */; }; 13EA64D02399D50C00FD8E13 /* DataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA64CF2399D50C00FD8E13 /* DataManager.swift */; }; + 1D2013F22DC4684E00E636DF /* DescriptionCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D2013F12DC4684E00E636DF /* DescriptionCard.swift */; }; + 1D2013F42DC4685E00E636DF /* CategoryOverviewCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D2013F32DC4685E00E636DF /* CategoryOverviewCard.swift */; }; 1D7DD6E02DC2EB5B00A6BBA7 /* GymDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D7DD6DF2DC2EB5B00A6BBA7 /* GymDetailView.swift */; }; 1DB006AD2D71C8D6001CC870 /* ResourcesSectionDropdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DB006AC2D71C8C0001CC870 /* ResourcesSectionDropdown.swift */; }; 1DB88F6D2D94DF78007713F7 /* OpenTimesCardSwiftUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DB88F6C2D94DF78007713F7 /* OpenTimesCardSwiftUIView.swift */; }; @@ -270,6 +272,8 @@ 13EA64C92399CE5B00FD8E13 /* SearchItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchItem.swift; sourceTree = ""; }; 13EA64CC2399CEDA00FD8E13 /* Gym.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Gym.swift; sourceTree = ""; }; 13EA64CF2399D50C00FD8E13 /* DataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataManager.swift; sourceTree = ""; }; + 1D2013F12DC4684E00E636DF /* DescriptionCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DescriptionCard.swift; sourceTree = ""; }; + 1D2013F32DC4685E00E636DF /* CategoryOverviewCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryOverviewCard.swift; sourceTree = ""; }; 1D7DD6DF2DC2EB5B00A6BBA7 /* GymDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GymDetailView.swift; sourceTree = ""; }; 1DB006AC2D71C8C0001CC870 /* ResourcesSectionDropdown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourcesSectionDropdown.swift; sourceTree = ""; }; 1DB88F6C2D94DF78007713F7 /* OpenTimesCardSwiftUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenTimesCardSwiftUIView.swift; sourceTree = ""; }; @@ -555,6 +559,8 @@ 1396013123865E2E005E4788 /* CardView.swift */, 298EE26F25BB82A2002BAF0F /* CardTableViewCell.swift */, 1396013223865E2E005E4788 /* TagView.swift */, + 1D2013F32DC4685E00E636DF /* CategoryOverviewCard.swift */, + 1D2013F12DC4684E00E636DF /* DescriptionCard.swift */, 018B982525327358004C3B26 /* ActionButton.swift */, 01B250272516AD4F00CBA459 /* IconPairView.swift */, 554CB99F23F3A83F00BB1715 /* EventTableViewCell.swift */, @@ -1176,9 +1182,11 @@ 13135BB3241244370056B169 /* FilterViewCell.swift in Sources */, 016A56D42519E96800531A12 /* CampusCalendarViewController.swift in Sources */, 298EE27025BB82A2002BAF0F /* CardTableViewCell.swift in Sources */, + 1D2013F22DC4684E00E636DF /* DescriptionCard.swift in Sources */, 13580AF62437D7E700D309AA /* LibraryDetailViewController.swift in Sources */, 135D7F77243A9BD1003F8BD1 /* Colors+GymClass.swift in Sources */, 55DCF7A123724835001B01B8 /* MaterialButton.swift in Sources */, + 1D2013F42DC4685E00E636DF /* CategoryOverviewCard.swift in Sources */, 297678892426D2C500FDD1EB /* SearchDrawerViewController.swift in Sources */, 29CB280A2404DD71009A2CFB /* FilterTableViewCell.swift in Sources */, 55DCF79623723CF2001B01B8 /* UIView+Extensions.swift in Sources */, diff --git a/berkeley-mobile/Common/CategoryOverviewCard.swift b/berkeley-mobile/Common/CategoryOverviewCard.swift new file mode 100644 index 000000000..922c768ad --- /dev/null +++ b/berkeley-mobile/Common/CategoryOverviewCard.swift @@ -0,0 +1,131 @@ +// +// CategoryOverviewCard.swift +// berkeley-mobile +// +// Created by Yihang Chen on 5/1/25. +// Copyright © 2025 ASUC OCTO. All rights reserved. +// + +import SwiftUI + +struct BMContactInfoRow: View { + let iconName: String + let text: String + var lineLimit: Int? = 1 + + var body: some View { + HStack(alignment: .top, spacing: 10) { + Image(systemName: iconName) + .frame(width: 18, height: 18) + + Text(text) + .font(Font(BMFont.light(12))) + .lineLimit(lineLimit) + .fixedSize(horizontal: false, vertical: true) + } + } +} + +struct BMCategoryOverviewCard: View { + let category: SearchItem & HasLocation & HasImage + + var body: some View { + HStack(alignment: .top, spacing: 16) { + VStack(alignment: .leading) { + titleView + + Spacer(minLength: 20) + + contactInfoView + } + + Spacer() + + imageView + } + .padding(12) + .background(Color(BMColor.cardBackground)) + .cornerRadius(12) + .shadow(color: Color(uiColor: .label).opacity(0.15), radius: 5, x: 0, y: 0) + .padding(.vertical, 8) + .padding(.horizontal, 4) + } + + private var titleView: some View { + Text(category.name) + .font(Font(BMFont.bold(23))) + .foregroundColor(Color(BMColor.blackText)) + .lineLimit(3) + .padding(.top, 8) + } + + private var contactInfoView: some View { + VStack(alignment: .leading, spacing: 12) { + if let address = category.address, !address.isEmpty { + BMContactInfoRow( + iconName: "location.fill", + text: address, + lineLimit: nil + ) + } + + if let hasPhone = category as? HasPhoneNumber, + let phoneNumber = hasPhone.phoneNumber, + !phoneNumber.isEmpty { + BMContactInfoRow( + iconName: "phone.fill", + text: phoneNumber + ) + } + + if let distance = category.distanceToUser { + BMContactInfoRow( + iconName: "figure.walk", + text: String(format: "%.1f miles", distance) + ) + } + } + .foregroundColor(Color(BMColor.blackText)) + } + + private var imageView: some View { + Group { + if let imageURL = category.imageURL { + AsyncImage(url: imageURL) { phase in + switch phase { + case .empty: + defaultImage + case .success(let image): + image + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 120, height: 200) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color(uiColor: .systemGray4), lineWidth: 0.5) + ) + case .failure: + defaultImage + @unknown default: + EmptyView() + } + } + } else { + defaultImage + } + } + } + + private var defaultImage: some View { + Image(uiImage: Constants.doeGladeImage) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 120, height: 200) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color(uiColor: .systemGray4), lineWidth: 0.5) + ) + } +} \ No newline at end of file diff --git a/berkeley-mobile/Common/DescriptionCard.swift b/berkeley-mobile/Common/DescriptionCard.swift new file mode 100644 index 000000000..8936ceb60 --- /dev/null +++ b/berkeley-mobile/Common/DescriptionCard.swift @@ -0,0 +1,29 @@ +// +// DescriptionCard.swift +// berkeley-mobile +// +// Created by Yihang Chen on 5/1/25. +// Copyright © 2025 ASUC OCTO. All rights reserved. +// + +import SwiftUI + +struct BMDescriptionCard: View { + let description: String + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + Text("Description") + .font(Font(BMFont.bold(16))) + .foregroundColor(Color(BMColor.blackText)) + + Text(description) + .font(Font(BMFont.light(12))) + .foregroundColor(Color(BMColor.blackText)) + } + .padding(16) + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color(BMColor.cardBackground)) + .cornerRadius(12) + } +} \ No newline at end of file diff --git a/berkeley-mobile/Fitness/GymDetailView.swift b/berkeley-mobile/Fitness/GymDetailView.swift index aed7e6f80..608bfdaed 100644 --- a/berkeley-mobile/Fitness/GymDetailView.swift +++ b/berkeley-mobile/Fitness/GymDetailView.swift @@ -7,22 +7,40 @@ import SwiftUI +struct ContactInfoRow: View { + let iconName: String + let text: String + var lineLimit: Int? = 1 + + var body: some View { + HStack(alignment: .top, spacing: 10) { + Image(systemName: iconName) + .frame(width: 18, height: 18) + + Text(text) + .font(Font(BMFont.light(12))) + .lineLimit(lineLimit) + .fixedSize(horizontal: false, vertical: true) + } + } +} + struct CategoryOverviewCard: View { let category: SearchItem & HasLocation & HasImage var body: some View { HStack(alignment: .top, spacing: 16) { VStack(alignment: .leading) { - TitleView(name: category.name) + titleView Spacer(minLength: 20) - ContactInfoView(category: category) + contactInfoView } Spacer() - ImageView(imageURL: category.imageURL) + imageView } .padding(12) .background(Color(BMColor.cardBackground)) @@ -32,74 +50,46 @@ struct CategoryOverviewCard: View { .padding(.horizontal, 4) } - struct TitleView: View { - let name: String - - var body: some View { - Text(name) - .font(Font(BMFont.bold(23))) - .foregroundColor(Color(BMColor.blackText)) - .lineLimit(3) - .padding(.top, 8) - } - } - - struct ContactInfoView: View { - let category: SearchItem & HasLocation - - var body: some View { - VStack(alignment: .leading, spacing: 12) { - if let address = category.address, !address.isEmpty { - ContactInfoRow( - iconName: "location.fill", - text: address, - lineLimit: nil - ) - } - - if let hasPhone = category as? HasPhoneNumber, - let phoneNumber = hasPhone.phoneNumber, - !phoneNumber.isEmpty { - ContactInfoRow( - iconName: "phone.fill", - text: phoneNumber - ) - } - - if let distance = category.distanceToUser { - ContactInfoRow( - iconName: "figure.walk", - text: String(format: "%.1f miles", distance) - ) - } - } + private var titleView: some View { + Text(category.name) + .font(Font(BMFont.bold(23))) .foregroundColor(Color(BMColor.blackText)) - } + .lineLimit(3) + .padding(.top, 8) } - struct ContactInfoRow: View { - let iconName: String - let text: String - var lineLimit: Int? = 1 - - var body: some View { - HStack(alignment: .top, spacing: 10) { - Image(systemName: iconName) - .frame(width: 18, height: 18) - - Text(text) - .font(Font(BMFont.light(12))) - .lineLimit(lineLimit) - .fixedSize(horizontal: false, vertical: true) + private var contactInfoView: some View { + VStack(alignment: .leading, spacing: 12) { + if let address = category.address, !address.isEmpty { + ContactInfoRow( + iconName: "location.fill", + text: address, + lineLimit: nil + ) + } + + if let hasPhone = category as? HasPhoneNumber, + let phoneNumber = hasPhone.phoneNumber, + !phoneNumber.isEmpty { + ContactInfoRow( + iconName: "phone.fill", + text: phoneNumber + ) + } + + if let distance = category.distanceToUser { + ContactInfoRow( + iconName: "figure.walk", + text: String(format: "%.1f miles", distance) + ) } } + .foregroundColor(Color(BMColor.blackText)) } - struct ImageView: View { - let imageURL: URL? - - var body: some View { - if let imageURL = imageURL { + private var imageView: some View { + Group { + if let imageURL = category.imageURL { AsyncImage(url: imageURL) { phase in switch phase { case .empty: @@ -124,18 +114,18 @@ struct CategoryOverviewCard: View { defaultImage } } - - private var defaultImage: some View { - Image(uiImage: Constants.doeGladeImage) - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 120, height: 200) - .clipShape(RoundedRectangle(cornerRadius: 8)) - .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(Color(uiColor: .systemGray4), lineWidth: 0.5) - ) - } + } + + private var defaultImage: some View { + Image(uiImage: Constants.doeGladeImage) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 120, height: 200) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color(uiColor: .systemGray4), lineWidth: 0.5) + ) } } @@ -150,7 +140,7 @@ struct GymDetailView: View { var body: some View { ScrollView { VStack(spacing: 16) { - CategoryOverviewCard(category: gym) + BMCategoryOverviewCard(category: gym) if gym.weeklyHours != nil { OpenTimesCardSwiftUIView(item: gym) @@ -163,7 +153,7 @@ struct GymDetailView: View { } if let description = gym.description, !description.isEmpty { - descriptionCard(description: description) + BMDescriptionCard(description: description) } } .padding(.horizontal, 16) @@ -171,22 +161,6 @@ struct GymDetailView: View { } .navigationTitle(gym.name) } - - private func descriptionCard(description: String) -> some View { - VStack(alignment: .leading, spacing: 10) { - Text("Description") - .font(Font(BMFont.bold(16))) - .foregroundColor(Color(BMColor.blackText)) - - Text(description) - .font(Font(BMFont.light(12))) - .foregroundColor(Color(BMColor.blackText)) - } - .padding(16) - .frame(maxWidth: .infinity, alignment: .leading) - .background(Color(BMColor.cardBackground)) - .cornerRadius(12) - } } #Preview { @@ -203,7 +177,5 @@ struct GymDetailView: View { sampleGym.latitude = 37.8687 sampleGym.longitude = -122.2614 - return NavigationView { - GymDetailView(gym: sampleGym) - } + return GymDetailView(gym: sampleGym) }