如何在MKMapView上实现全球昼夜区域覆盖层显示?
在MKMapView实现昼夜区域覆盖层
要实现夜间区域黑色不透明覆盖、白天无覆盖的效果,核心是计算当前时刻的晨昏线坐标,生成覆盖黑夜半球的MKPolygon作为Overlay,再通过自定义渲染器完成图层绘制。以下是完整实现方案:
关键实现思路
- 晨昏线坐标计算:基于当前UTC时间,利用天文公式推导太阳赤纬角、时间角,进而生成昼夜分界线的经纬度点集合。
- 黑夜区域多边形生成:将晨昏线点连接,并补充南北极点坐标,形成闭合的黑夜区域多边形。
- SwiftUI集成与实时更新:通过
UIViewRepresentable管理Overlay的添加/移除,配合定时器实现昼夜区域随时间动态刷新。
完整代码实现
import SwiftUI import MapKit import CoreLocation // 天文计算工具:生成晨昏线坐标 struct SunGeometry { static func twilightCoordinates(for date: Date = Date()) -> [CLLocationCoordinate2D] { let calendar = Calendar(identifier: .gregorian) let utcDate = calendar.date(byAdding: .second, value: -TimeZone.current.secondsFromGMT(), to: date)! let julianDate = julianDay(from: utcDate) // 计算太阳赤纬角(弧度) let dayOfYear = julianDayOfYear(from: utcDate) let delta = degreesToRadians(23.4393 * sin((2 * .pi / 365) * (284 + dayOfYear))) // 计算格林威治时间角(弧度) let hours = calendar.component(.hour, from: utcDate) + Double(calendar.component(.minute, from: utcDate))/60 + Double(calendar.component(.second, from: utcDate))/3600 let gamma = degreesToRadians(15 * (hours - 12)) var coordinates = [CLLocationCoordinate2D]() // 生成晨昏线经纬度点(每隔1度取点) for lon in stride(from: -180, through: 180, by: 1) { let lambda = degreesToRadians(Double(lon)) let tanPhi = (-sin(gamma) * cos(delta)) / (cos(lambda) * sin(delta)) let phi = atan(tanPhi) coordinates.append(CLLocationCoordinate2D(latitude: radiansToDegrees(phi), longitude: Double(lon))) } // 补充南北极点,闭合多边形覆盖整个黑夜半球 coordinates.append(CLLocationCoordinate2D(latitude: -90, longitude: 0)) coordinates.append(CLLocationCoordinate2D(latitude: 90, longitude: 0)) return coordinates } // 转换为儒略日 private static func julianDay(from date: Date) -> Double { let components = Calendar(identifier: .gregorian).dateComponents([.year, .month, .day, .hour, .minute, .second], from: date) let year = Double(components.year!) let month = Double(components.month!) let day = Double(components.day!) + Double(components.hour!)/24 + Double(components.minute!)/1440 + Double(components.second!)/86400 return month <= 2 ? 367 * year - floor(7 * (year + floor((month + 9)/12))/4) + floor(275 * month/9) + day + 1721013.5 : 367 * year - floor(7 * (year + floor((month + 9)/12))/4) + floor(275 * month/9) + day + 1721013.5 } // 计算年内儒略日 private static func julianDayOfYear(from date: Date) -> Double { let startOfYear = Calendar(identifier: .gregorian).date(from: Calendar(identifier: .gregorian).dateComponents([.year], from: date))! return date.timeIntervalSince(startOfYear) / 86400 + 1 } private static func degreesToRadians(_ degrees: Double) -> Double { degrees * .pi / 180 } private static func radiansToDegrees(_ radians: Double) -> Double { radians * 180 / .pi } } // 自定义黑夜覆盖层渲染器 class NightOverlayRenderer: MKPolygonRenderer { override init(overlay: MKOverlay) { super.init(overlay: overlay) fillColor = UIColor.black.withAlphaComponent(0.7) // 可调整透明度 strokeColor = .clear } } struct OurMapView: UIViewRepresentable { @State private var nightOverlay: MKPolygon? func makeUIView(context: Context) -> MKMapView { let mapView = MKMapView() mapView.showsUserLocation = true mapView.mapType = .standard mapView.delegate = context.coordinator // 初始化昼夜覆盖层 updateNightOverlay(on: mapView) // 每分钟更新一次昼夜区域(可按需调整频率) Timer.scheduledTimer(withTimeInterval: 60, repeats: true) { _ in updateNightOverlay(on: mapView) } return mapView } func updateUIView(_ uiView: MKMapView, context: Context) {} func makeCoordinator() -> Coordinator { Coordinator(self) } class Coordinator: NSObject, MKMapViewDelegate { var parent: OurMapView init(_ parent: OurMapView) { self.parent = parent } func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { guard let polygon = overlay as? MKPolygon else { return MKOverlayRenderer(overlay: overlay) } return NightOverlayRenderer(overlay: polygon) } } // 更新黑夜覆盖层 private func updateNightOverlay(on mapView: MKMapView) { nightOverlay.map { mapView.removeOverlay($0) } let coordinates = SunGeometry.twilightCoordinates() let polygon = MKPolygon(coordinates: coordinates, count: coordinates.count) nightOverlay = polygon mapView.addOverlay(polygon) } } // 预览 struct OurMapView_Previews: PreviewProvider { static var previews: some View { OurMapView() } }
细节说明
- 天文计算逻辑:
SunGeometry类中的公式基于简化的天文模型,日常使用精度足够;若需更高精度,可引入专业天文计算库。 - 渲染控制:通过
NightOverlayRenderer的fillColor可调整黑夜覆盖层的透明度,满足不同视觉需求。 - 实时更新:定时器每分钟刷新一次晨昏线坐标,确保昼夜区域随时间动态变化。
内容的提问来源于stack exchange,提问作者RinaredTap
相关产品推荐
相关产品推荐

