I Have been trying to figure this problem out but without success.
I need to update the map view based on the current acitivity that is shown in the scrollview.
the. .Ontap changes it well but scrolling seems to to update the scrollposition in the scrollview.
Anyone that does know how to make this work ?
Thanks a lot
import SwiftUI
import CoreData
import MapKit
struct LandingView: View {
@Environment(.managedObjectContext) private var viewContext
@State private var region = MKCoordinateRegion()
@State private var activeTab: String = "Today"
@State private var scrollViewHeight: CGFloat = .zero
@State private var activitiesOfTheDay: [Activity] = []
@State private var activitiesOfNextDay: [Activity] = []
@State private var todayText: String = "Today"
@State private var tomorrowText: String = "Tomorrow"
@State private var airportData: [String: AirportInfo] = [:]
@State private var depCoordinate: CLLocationCoordinate2D?
@State private var arrCoordinate: CLLocationCoordinate2D?
@State private var selectedActivity: Activity?
private let excludedActivityPrefixes = [
"W00", "W01", "W02", "W03", "W04", "W05", "W06", "W07", "W08", "W09", "W10", "W11", "W12",
"W13", "W14", "W15", "W16", "W17", "W18", "W19", "W20", "W21", "W22", "W23",
"WDI", "WDO", "WF", "WJV", "WLS", "WSD", "WVA"
]
var body: some View {
VStack(spacing: 0) {
ZStack(alignment: .topLeading) {
MapView(region: $region, depCoordinate: $depCoordinate, arrCoordinate: $arrCoordinate)
.onAppear {
loadAirportData()
fetchActivities()
}
.ignoresSafeArea(edges: .top)
Button(action: {
// Action for the button
}) {
Image(systemName: "line.horizontal.3")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.padding(10)
}
.background(Color.white)
.clipShape(Circle())
.shadow(radius: 2)
.padding(.leading)
VStack {
Spacer()
HStack {
Button(action: {
// Action for the button
}) {
Text("See Full Roster")
.foregroundColor(.white)
.padding(8)
.background(Color.green)
.cornerRadius(20)
}
.padding(.leading)
.padding(.bottom)
Spacer()
HStack {
Button(action: {
activeTab = "Today"
if let activity = activitiesOfTheDay.first {
setCoordinates(for: activity)
}
}) {
Text(todayText)
.foregroundColor(activeTab == "Today" ? Color.green : Color.green.opacity(0.3))
.padding(8)
}
Button(action: {
activeTab = "Tomorrow"
if let activity = activitiesOfNextDay.first {
setCoordinates(for: activity)
}
}) {
Text(tomorrowText)
.foregroundColor(activeTab == "Tomorrow" ? Color.green : Color.green.opacity(0.3))
.padding(8)
}
}
.background(Color.white)
.cornerRadius(20)
.padding(.trailing)
.padding(.bottom)
}
}
}
.frame(maxHeight: 225)
if (activeTab == "Today" ? activitiesOfTheDay : activitiesOfNextDay).count == 1 {
if let activity = (activeTab == "Today" ? activitiesOfTheDay : activitiesOfNextDay).first {
ActivityView(activity: activity)
.frame(width: UIScreen.main.bounds.width * 0.8)
.background(GeometryReader {
Color.clear.preference(key: ViewHeightKey.self, value: $0.size.height)
})
.onAppear {
setCoordinates(for: activity, zoomOut: true)
}
}
} else {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(activeTab == "Today" ? activitiesOfTheDay : activitiesOfNextDay) { activity in
ZStack { // Wrap each activity in a ZStack
ActivityView(activity: activity)
.onTapGesture {
self.selectedActivity = activity
}
.frame(width: UIScreen.main.bounds.width * 0.8) // Adjust width as needed
.background(GeometryReader {
Color.clear.preference(key: ViewHeightKey.self, value: $0.size.height)
})
}
}
}
.scrollTargetLayout()
}
.scrollTargetBehavior(.viewAligned)
.scrollPosition(id: $selectedActivity)
.safeAreaPadding(.horizontal, 0)
.frame(maxHeight: scrollViewHeight)
.onPreferenceChange(ViewHeightKey.self) { height in
scrollViewHeight = height
}
.onReceive([self.selectedActivity].publisher.first()) { newPosition in
if let newActivity = newPosition {
print("Selected activity dep: (newActivity.dep ?? "Unknown")")
setCoordinates(for: newActivity)
}
}
}
Divider()
ListView(activities: activeTab == "Today" ? activitiesOfTheDay : activitiesOfNextDay)
Spacer()
}
.ignoresSafeArea(edges: .bottom)
}
private func setInitialRegion(from depCoordinate: CLLocationCoordinate2D, to arrCoordinate: CLLocationCoordinate2D) {
let centerLatitude = (depCoordinate.latitude + arrCoordinate.latitude) / 2
let centerLongitude = (depCoordinate.longitude + arrCoordinate.longitude) / 2
let spanLatitudeDelta = abs(depCoordinate.latitude - arrCoordinate.latitude) * 1.5
let spanLongitudeDelta = abs(depCoordinate.longitude - arrCoordinate.longitude) * 1.5
region = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: centerLatitude, longitude: centerLongitude),
span: MKCoordinateSpan(latitudeDelta: spanLatitudeDelta, longitudeDelta: spanLongitudeDelta)
)
}
private func setInitialRegion(for coordinate: CLLocationCoordinate2D, zoomOut: Bool) {
let spanDelta = zoomOut ? 0.2 : 0.1 // Zoom out a bit more if specified
region = MKCoordinateRegion(
center: coordinate,
span: MKCoordinateSpan(latitudeDelta: spanDelta, longitudeDelta: spanDelta)
)
}
private func loadAirportData() {
if let data = CSVLoader.loadAirportData(from: "iata-icao") {
airportData = data
}
}
private func fetchActivities() {
let fetchRequest: NSFetchRequest<Activity> = Activity.fetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: Activity.start, ascending: true)]
// fetchRequest.predicate = NSPredicate(format: "start >= %@", Date() as NSDate)
do {
let activities = try viewContext.fetch(fetchRequest)
let filteredActivities = activities.filter { activity in
!excludedActivityPrefixes.contains(where: { activity.activity?.hasPrefix($0) == true })
}
if let firstDayActivity = filteredActivities.first {
let calendar = Calendar.current
let firstDay = calendar.startOfDay(for: firstDayActivity.start!)
activitiesOfTheDay = filteredActivities.filter { calendar.isDate($0.start!, inSameDayAs: firstDay) }
if let nextDayActivity = filteredActivities.first(where: { !calendar.isDate($0.start!, inSameDayAs: firstDay) }) {
let nextDay = calendar.startOfDay(for: nextDayActivity.start!)
activitiesOfNextDay = filteredActivities.filter { calendar.isDate($0.start!, inSameDayAs: nextDay) }
todayText = calendar.isDateInToday(firstDay) ? "Today" : formatDate(firstDay)
tomorrowText = calendar.isDateInToday(nextDay) ? "Tomorrow" : formatDate(nextDay)
if activeTab == "Today", let activity = activitiesOfTheDay.first {
setCoordinates(for: activity)
} else if activeTab == "Tomorrow", let activity = activitiesOfNextDay.first {
setCoordinates(for: activity)
}
} else {
tomorrowText = "No Activities"
}
} else {
todayText = "No Activities"
tomorrowText = "No Activities"
}
} catch {
print("Failed to fetch activities: (error)")
}
}
private func setCoordinates(for activity: Activity, zoomOut: Bool = false) {
if let depCode = activity.dep, let depData = airportData[depCode],
let arrCode = activity.arr, let arrData = airportData[arrCode] {
depCoordinate = CLLocationCoordinate2D(latitude: depData.latitude, longitude: depData.longitude)
if depCode == arrCode {
arrCoordinate = nil
if let depCoordinate = depCoordinate {
setInitialRegion(for: depCoordinate, zoomOut: true)
}
} else {
arrCoordinate = CLLocationCoordinate2D(latitude: arrData.latitude, longitude: arrData.longitude)
if let depCoordinate = depCoordinate, let arrCoordinate = arrCoordinate {
setInitialRegion(from: depCoordinate, to: arrCoordinate)
}
}
}
}
private func formatDate(_ date: Date) -> String {
let formatter = DateFormatter()
formatter.dateStyle = .medium
return formatter.string(from: date)
}
}
struct ViewHeightKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = max(value, nextValue())
}
}
struct MapView: UIViewRepresentable {
@Binding var region: MKCoordinateRegion
@Binding var depCoordinate: CLLocationCoordinate2D?
@Binding var arrCoordinate: CLLocationCoordinate2D?
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.delegate = context.coordinator
mapView.showsUserLocation = true
mapView.isZoomEnabled = true
mapView.isScrollEnabled = true
mapView.pointOfInterestFilter = .excludingAll
updateAnnotationsAndPolyline(mapView: mapView)
mapView.setRegion(region, animated: true)
return mapView
}
func updateUIView(_ uiView: MKMapView, context: Context) {
uiView.setRegion(region, animated: true)
updateAnnotationsAndPolyline(mapView: uiView)
}
func updateAnnotationsAndPolyline(mapView: MKMapView) {
mapView.removeAnnotations(mapView.annotations)
if let depCoordinate = depCoordinate {
let depAnnotation = MKPointAnnotation()
depAnnotation.coordinate = depCoordinate
depAnnotation.title = "Departure"
mapView.addAnnotation(depAnnotation)
}
if let arrCoordinate = arrCoordinate {
let arrAnnotation = MKPointAnnotation()
arrAnnotation.coordinate = arrCoordinate
arrAnnotation.title = "Arrival"
mapView.addAnnotation(arrAnnotation)
}
mapView.removeOverlays(mapView.overlays)
if let depCoordinate = depCoordinate, let arrCoordinate = arrCoordinate {
let coordinates = [depCoordinate, arrCoordinate]
let polyline = MKPolyline(coordinates: coordinates, count: coordinates.count)
mapView.addOverlay(polyline)
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self, region: $region)
}
class Coordinator: NSObject, MKMapViewDelegate {
var parent: MapView
var region: Binding<MKCoordinateRegion>
init(_ parent: MapView, region: Binding<MKCoordinateRegion>) {
self.parent = parent
self.region = region
}
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
if let polyline = overlay as? MKPolyline {
let renderer = MKPolylineRenderer(polyline: polyline)
renderer.strokeColor = .blue
renderer.lineWidth = 2
return renderer
}
return MKOverlayRenderer()
}
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
let identifier = "AirportAnnotation"
var view: CustomAnnotationView
if let dequeuedView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? CustomAnnotationView {
dequeuedView.annotation = annotation
view = dequeuedView
} else {
view = CustomAnnotationView(annotation: annotation, reuseIdentifier: identifier)
view.canShowCallout = true
}
return view
}
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
let region = MKCoordinateRegion(
center: mapView.region.center,
span: MKCoordinateSpan(
latitudeDelta: mapView.region.span.latitudeDelta,
longitudeDelta: mapView.region.span.longitudeDelta)
)
self.region.wrappedValue = region
}
}
}
struct Runway {
let startCoordinate: CLLocationCoordinate2D
let endCoordinate: CLLocationCoordinate2D
let widthInFeet: Double
}
extension Double {
var degreesToRadians: Double { self * .pi / 180 }
var radiansToDegrees: Double { self * 180 / .pi }
}
#Preview {
LandingView()
}
expect it to update the scrollposition as it should in ios17
New contributor
Max van Bruinessen is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.