Skip to content

Commit

Permalink
Merge pull request #517 from pennlabs/dining-scraping
Browse files Browse the repository at this point in the history
Cool Dining UI changes to make UX radically more enjoyable
  • Loading branch information
jonathanmelitski authored Mar 22, 2024
2 parents 5f1e0db + 31b8c90 commit 199a167
Show file tree
Hide file tree
Showing 6 changed files with 513 additions and 220 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,101 +2,100 @@
// DiningVenueDetailMenuView.swift
// PennMobile
//
// Created by CHOI Jongmin on 23/6/2020.
// Copyright © 2020 PennLabs. All rights reserved.
// Created by Jon Melitski on 2/26/2024.
// Copyright © 2024 PennLabs. All rights reserved.
//

import SwiftUI
import WebKit
import PennMobileShared

struct DiningVenueDetailMenuView: View {
var menus: [DiningMenu]

@EnvironmentObject var diningVM: DiningViewModelSwiftUI

var id: Int
var venue: DiningVenue?
var venue: DiningVenue
var parentScrollProxy: ScrollViewProxy

/// Notable invariant, the menuDate must ALWAYS match all of the menus in the array.
@State var menuDate: Date
@State private var menuIndex: Int
@State private var showMenu: Bool
@EnvironmentObject var diningVM: DiningViewModelSwiftUI
init(menus: [DiningMenu], id: Int, venue: DiningVenue? = nil, menuDate: Date = Date(), showMenu: Bool = false) {
self.menus = menus
@State var menus: [DiningMenu]

// Both are nil on init
@State private var currentMenu: DiningMenu?
@State private var selectedStation: DiningStation?

@Binding private var parentScrollOffset: CGPoint

init(menus: [DiningMenu], id: Int, venue: DiningVenue, menuDate: Date = Date(), parentScrollProxy: ScrollViewProxy, parentScrollOffset: Binding<CGPoint>) {
self.id = id
self.venue = venue
_showMenu = State(initialValue: showMenu)
self.parentScrollProxy = parentScrollProxy
_parentScrollOffset = parentScrollOffset
_menus = State(initialValue: menus)
_menuDate = State(initialValue: menuDate)
_menuIndex = State(initialValue: 0)
_menuIndex = State(initialValue: self.getIndex())
_currentMenu = State(initialValue: getMenu())
_selectedStation = State(initialValue: currentMenu?.stations.first ?? nil)

}
func getIndex() -> Int {
var inx = 0
if self.venue != nil && Calendar.current.isDate(self.menuDate, inSameDayAs: Date()) {
if let meal = self.venue!.currentOrNearestMeal {
inx = self.menus.firstIndex { $0.service == meal.label } ?? inx
}

/// Constraints of this function:
/// Need to know if a meal is currently going on, to return it.
/// If there is a meal today that is closest (utilities), return it.
/// If the selected date is not the current day, return the first menu.
/// If at any point, the list of menus is empty, return nil.
func getMenu() -> DiningMenu? {
if (menus.count == 0) { return nil }

if (!Calendar.current.isDate(menuDate, inSameDayAs: Date())) {
return menus[0]
}
return inx

guard let nearestIndex = venue.currentOrNearestMealIndex else {
return nil
}

return menus[nearestIndex]
}

var body: some View {
DatePicker(selection: $menuDate, in: Date()...Date().addingTimeInterval(86400 * 6), displayedComponents: .date) {
Text("Menu date")
}.onChange(of: menuDate) { newMenuDate in
menuIndex = 0
Task.init() {
await diningVM.refreshMenus(cache: false, at: newMenuDate)
}
if Calendar.current.isDate(newMenuDate, inSameDayAs: Date()) {
menuIndex = getIndex()
}
}
VStack {
Button {
showMenu.toggle()
} label: {
CardView {
HStack {
Text("Menu")
.font(.system(size: 20, design: .rounded))
.bold()
Spacer()
Image(systemName: "chevron.right")
}
.padding()
.foregroundColor(.blue).font(Font.system(size: 24).weight(.bold))
LazyVStack(pinnedViews: [.sectionHeaders]) {
HStack {
if currentMenu != nil {
Picker("Menu", selection: Binding($currentMenu)!) {
ForEach(menus, id: \.self) { menu in
Text(menu.service)
}
}.pickerStyle(MenuPickerStyle())
} else {
Text("Closed For Today")
}
.frame(height: 24)
.padding([.top, .bottom])
}
.sheet(isPresented: $showMenu) {
WebView(url: URL(string: DiningVenue.menuUrlDict[id] ?? "https://university-of-pennsylvania.cafebonappetit.com/")!)
DatePicker("", selection: $menuDate, in: Date()...Date().add(minutes: 8640), displayedComponents: .date)
}
if menus.count > 0 {
Picker("Menu", selection: self.$menuIndex) {
ForEach(0 ..< menus.count, id: \.self) {
Text(menus[$0].service)
}

Section {
DiningStationRowStack(selectedStation: $selectedStation, currentMenu: $currentMenu, parentScrollOffset: $parentScrollOffset, parentScrollProxy: parentScrollProxy)
} header: {
VStack {
DiningMenuViewHeader(diningMenu: $currentMenu, selectedStation: $selectedStation)
}
.pickerStyle(SegmentedPickerStyle())
DiningMenuRow(diningMenu: menus[menuIndex])
.transition(.opacity)
}
}
}
}

struct DiningVenueDetailMenuView_Previews: PreviewProvider {
let diningVenues: MenuList = Bundle.main.decode("mock_menu.json")

static var previews: some View {
return NavigationView {
ScrollView {
VStack {
DiningVenueDetailMenuView(menus: [], id: 1)
Spacer()
}
}.navigationTitle("Dining")
.padding()

.onChange(of: currentMenu) { _ in
print((currentMenu?.service ?? "no menu") + " on " + menuDate.description)
selectedStation = currentMenu?.stations.first ?? nil
}
.onChange(of: menuDate) { newDate in
Task.init() {
await diningVM.refreshMenus(cache: true, at: newDate)
menuDate = newDate
menus = diningVM.diningMenus[venue.id]?.menus ?? []
currentMenu = getMenu()
}
}

}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import SwiftUI
import Kingfisher
import FirebaseAnalytics
import PennMobileShared
import WebKit

struct DiningVenueDetailView: View {

Expand All @@ -23,84 +24,116 @@ struct DiningVenueDetailView: View {
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
@EnvironmentObject var diningVM: DiningViewModelSwiftUI
@State private var pickerIndex = 0
@State private var contentOffset: CGPoint = .zero
@State private var showMenu = false
@State var showTitle = false

var body: some View {
GeometryReader { fullGeo in
let imageHeight = fullGeo.size.height * 4/9
let isFavorite = diningVM.favoriteVenues.contains { $0.id == venue.id }

ScrollView {
GeometryReader { geometry in
let minY = geometry.frame(in: .global).minY

ZStack(alignment: .bottomLeading) {
KFImage(self.venue.image)
.resizable()
.scaledToFill()
.frame(width: geometry.size.width, height: imageHeight + max(0, minY))
.offset(y: min(0, minY) * -2/3)
.allowsHitTesting(false)
.clipped()

LinearGradient(gradient: Gradient(colors: [.black.opacity(0.6), .black.opacity(0.2), .clear, .black.opacity(0.3), .black]), startPoint: .init(x: 0.5, y: 0.2), endPoint: .init(x: 0.5, y: 1))

Text(venue.name)
.padding()
.foregroundColor(.white)
.font(.system(size: 40, weight: .bold))
.minimumScaleFactor(0.2)
.lineLimit(1)
.background(GeometryReader { geometry in
let minY = geometry.frame(in: .global).minY
Color.clear.onChange(of: minY) { minY in
showTitle = minY <= 64
}
})
}
.offset(y: -max(0, minY))
}
.edgesIgnoringSafeArea(.all)
.frame(height: imageHeight)
.zIndex(2)

VStack(spacing: 10) {
Picker("Section", selection: self.$pickerIndex) {
ForEach(0 ..< self.sectionTitle.count, id: \.self) {
Text(self.sectionTitle[$0])
ScrollViewReader { fullReader in
ScrollView {
// Image and Name
GeometryReader { geometry in
let minY = geometry.frame(in: .global).minY

ZStack(alignment: .bottomLeading) {
KFImage(self.venue.image)
.resizable()
.scaledToFill()
.frame(width: geometry.size.width, height: imageHeight + max(0, minY))
.offset(y: min(0, minY) * -2/3)
.allowsHitTesting(false)
.clipped()

LinearGradient(gradient: Gradient(colors: [.black.opacity(0.6), .black.opacity(0.2), .clear, .black.opacity(0.3), .black]), startPoint: .init(x: 0.5, y: 0.2), endPoint: .init(x: 0.5, y: 1))

Text(venue.name)
.padding()
.foregroundColor(.white)
.font(.system(size: 40, weight: .bold))
.minimumScaleFactor(0.2)
.lineLimit(1)
.background(GeometryReader { geometry in
let minY = geometry.frame(in: .global).minY
Color.clear.onChange(of: minY) { minY in
showTitle = minY <= 64
}
})
}
.offset(y: -max(0, minY))
}
.pickerStyle(SegmentedPickerStyle())

Divider()

VStack {
if self.pickerIndex == 0 {
DiningVenueDetailMenuView(menus: diningVM.diningMenus[venue.id]?.menus ?? [], id: venue.id, venue: venue)
} else if self.pickerIndex == 1 {
DiningVenueDetailHoursView(for: venue)
} else {
DiningVenueDetailLocationView(for: venue, screenHeight: fullGeo.size.width)
.edgesIgnoringSafeArea(.all)
.frame(height: imageHeight)
.zIndex(2)

VStack(spacing: 10) {
HStack (spacing: 10) {
Picker("Section", selection: self.$pickerIndex) {
ForEach(0 ..< self.sectionTitle.count, id: \.self) {
Text(self.sectionTitle[$0])
}
}
.pickerStyle(SegmentedPickerStyle())

Button {
showMenu.toggle()
} label: {
Image(systemName:"safari")
.font(.largeTitle)
}
.sheet(isPresented: $showMenu) {
WebView(url: URL(string: DiningVenue.menuUrlDict[venue.id] ?? "https://university-of-pennsylvania.cafebonappetit.com/")!)
}
}

Spacer()
}.frame(minHeight: fullGeo.size.height - 80)
}.padding(.horizontal)
}
.navigationTitle(Text(showTitle ? venue.name : ""))
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem {
Button(action: isFavorite ? { diningVM.removeVenueFromFavorites(venue: venue) } : { diningVM.addVenueToFavorites(venue: venue) }) {
Image(systemName: isFavorite ? "star.fill" : "star")
.font(.system(size: 20, weight: .light))

Divider()

VStack {
if self.pickerIndex == 0 {
DiningVenueDetailMenuView(menus: diningVM.diningMenus[venue.id]?.menus ?? [], id: venue.id, venue: venue, parentScrollProxy: fullReader,
parentScrollOffset: $contentOffset)
} else if self.pickerIndex == 1 {
DiningVenueDetailHoursView(for: venue)
} else {
DiningVenueDetailLocationView(for: venue, screenHeight: fullGeo.size.width)
}
Spacer()
}.frame(minHeight: fullGeo.size.height - 80)
}.padding(.horizontal)
.background(GeometryReader { geometry in
Color.clear
.preference(key: ScrollOffsetPreferenceKey.self, value: geometry.frame(in: .named("scroll")).origin)
})
.onPreferenceChange(ScrollOffsetPreferenceKey.self) { value in
self.contentOffset = value
}

}
.coordinateSpace(name: "scroll")
.navigationTitle(Text(showTitle ? venue.name : ""))
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem {
Button(action: isFavorite ? { diningVM.removeVenueFromFavorites(venue: venue) } : { diningVM.addVenueToFavorites(venue: venue) }) {
Image(systemName: isFavorite ? "star.fill" : "star")
.font(.system(size: 20, weight: .light))
}
.tint(.yellow)
}
.tint(.yellow)
}
.onAppear {
FirebaseAnalyticsManager.shared.trackScreen("Venue Detail View")
}
}
.onAppear {
FirebaseAnalyticsManager.shared.trackScreen("Venue Detail View")
}
}
}

struct ScrollOffsetPreferenceKey: PreferenceKey {
static var defaultValue: CGPoint = .zero

static func reduce(value: inout CGPoint, nextValue: () -> CGPoint) {
}
}
}
Expand All @@ -113,3 +146,13 @@ extension UINavigationController {
interactivePopGestureRecognizer?.delegate = nil
}
}

struct WebView: UIViewRepresentable {

Check failure on line 150 in PennMobile/Dining/SwiftUI/Views/Venue/Detail View/DiningVenueDetailView.swift

View workflow job for this annotation

GitHub Actions / build

invalid redeclaration of 'WebView'
let url: URL
func makeUIView(context: Context) -> WKWebView {
return WKWebView()
}
func updateUIView(_ uiView: WKWebView, context: Context) {
uiView.load(URLRequest(url: url))
}
}
Loading

0 comments on commit 199a167

Please sign in to comment.