diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2022-07-22 13:40:38 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2022-07-22 13:40:38 +0200 |
| commit | d79cb0d9744d675b1815729269862c5a44f043d4 (patch) | |
| tree | 84e256156667756268f2de52e0cd51cfdbb56963 | |
| parent | 83e99dfa413ad6d9c34bc492317499c9c5d56526 (diff) | |
| parent | 9383fbced659038d7a13fe5f5aa55c76e09b0b7a (diff) | |
| download | mullvadvpn-d79cb0d9744d675b1815729269862c5a44f043d4.tar.xz mullvadvpn-d79cb0d9744d675b1815729269862c5a44f043d4.zip | |
Merge branch 'fix-map-camera'
| -rw-r--r-- | ios/CHANGELOG.md | 1 | ||||
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 8 | ||||
| -rw-r--r-- | ios/MullvadVPN/ConnectContentView.swift (renamed from ios/MullvadVPN/ConnectMainContentView.swift) | 4 | ||||
| -rw-r--r-- | ios/MullvadVPN/ConnectViewController.swift | 192 |
4 files changed, 118 insertions, 87 deletions
diff --git a/ios/CHANGELOG.md b/ios/CHANGELOG.md index 7847bb955f..ac31d645b4 100644 --- a/ios/CHANGELOG.md +++ b/ios/CHANGELOG.md @@ -30,6 +30,7 @@ Line wrap the file at 100 chars. Th ### Fixed - Improve random port distribution. Should be less biased towards port 53. +- Fix invalid map camera position during the app launch and keep it up to date when multitasking. ## [2022.2] - 2022-04-28 diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 693939135d..00ece54d46 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -229,7 +229,7 @@ 58B0A2AC238EE6D500BC001D /* IPAddress+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5840250022B1124600E4CFEC /* IPAddress+Codable.swift */; }; 58B0A2AD238EE6EC00BC001D /* MullvadEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5840250322B11AB700E4CFEC /* MullvadEndpoint.swift */; }; 58B3F30F2742708B00A2DD38 /* HeaderBarButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B3F30E2742708B00A2DD38 /* HeaderBarButton.swift */; }; - 58B43C1925F77DB60002C8C3 /* ConnectMainContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B43C1825F77DB60002C8C3 /* ConnectMainContentView.swift */; }; + 58B43C1925F77DB60002C8C3 /* ConnectContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B43C1825F77DB60002C8C3 /* ConnectContentView.swift */; }; 58B5A895280AACC4009FDE99 /* RESTRequestFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B5A894280AACC4009FDE99 /* RESTRequestFactory.swift */; }; 58B5A899280AB0D7009FDE99 /* RESTAuthorization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58B5A898280AB0D7009FDE99 /* RESTAuthorization.swift */; }; 58B67B482602079E008EF58E /* RelaySelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58781CD422AFBA39009B9D8E /* RelaySelector.swift */; }; @@ -516,7 +516,7 @@ 58B0A2A0238EE67E00BC001D /* MullvadVPNTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MullvadVPNTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 58B0A2A4238EE67E00BC001D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 58B3F30E2742708B00A2DD38 /* HeaderBarButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderBarButton.swift; sourceTree = "<group>"; }; - 58B43C1825F77DB60002C8C3 /* ConnectMainContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectMainContentView.swift; sourceTree = "<group>"; }; + 58B43C1825F77DB60002C8C3 /* ConnectContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectContentView.swift; sourceTree = "<group>"; }; 58B5A894280AACC4009FDE99 /* RESTRequestFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTRequestFactory.swift; sourceTree = "<group>"; }; 58B5A898280AB0D7009FDE99 /* RESTAuthorization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTAuthorization.swift; sourceTree = "<group>"; }; 58B93A1226C3F13600A55733 /* TunnelState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelState.swift; sourceTree = "<group>"; }; @@ -875,7 +875,7 @@ 587EB669270EFACB00123C75 /* CharacterSet+IPAddress.swift */, 582AD43F27BE616E002A6BFC /* CodingErrors+ChainedError.swift */, 58A1AA8B23F5584B009F7EA6 /* ConnectionPanelView.swift */, - 58B43C1825F77DB60002C8C3 /* ConnectMainContentView.swift */, + 58B43C1825F77DB60002C8C3 /* ConnectContentView.swift */, 58CCA00F224249A1004F3011 /* ConnectViewController.swift */, 584592602639B4A200EF967F /* TermsOfServiceContentView.swift */, 58A99ED2240014A0006599E9 /* TermsOfServiceViewController.swift */, @@ -1475,7 +1475,7 @@ 584D26C4270C855B004EA533 /* PreferencesDataSource.swift in Sources */, 58B5A899280AB0D7009FDE99 /* RESTAuthorization.swift in Sources */, 58FD5BF024238EB300112C88 /* SKProduct+Formatting.swift in Sources */, - 58B43C1925F77DB60002C8C3 /* ConnectMainContentView.swift in Sources */, + 58B43C1925F77DB60002C8C3 /* ConnectContentView.swift in Sources */, 58561C99239A5D1500BD6B5E /* IPEndpoint.swift in Sources */, 58F97A1E280FDE230050C2FC /* RESTRequestHandler.swift in Sources */, 58FD5BF22424F7D700112C88 /* UserInterfaceInteractionRestriction.swift in Sources */, diff --git a/ios/MullvadVPN/ConnectMainContentView.swift b/ios/MullvadVPN/ConnectContentView.swift index 40ffdce960..426f499c75 100644 --- a/ios/MullvadVPN/ConnectMainContentView.swift +++ b/ios/MullvadVPN/ConnectContentView.swift @@ -1,5 +1,5 @@ // -// ConnectMainContentView.swift +// ConnectContentView.swift // MullvadVPN // // Created by pronebird on 09/03/2021. @@ -9,7 +9,7 @@ import UIKit import MapKit -class ConnectMainContentView: UIView { +class ConnectContentView: UIView { enum ActionButton { case connect case disconnect diff --git a/ios/MullvadVPN/ConnectViewController.swift b/ios/MullvadVPN/ConnectViewController.swift index 44475e603b..053dde26a7 100644 --- a/ios/MullvadVPN/ConnectViewController.swift +++ b/ios/MullvadVPN/ConnectViewController.swift @@ -30,17 +30,18 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen let notificationController = NotificationController() - private let mainContentView: ConnectMainContentView = { - let view = ConnectMainContentView(frame: UIScreen.main.bounds) + private let contentView: ConnectContentView = { + let view = ConnectContentView(frame: UIScreen.main.bounds) view.translatesAutoresizingMaskIntoConstraints = false return view }() private let logger = Logger(label: "ConnectViewController") - private var lastLocation: CLLocationCoordinate2D? + private var targetRegion: MKCoordinateRegion? private let locationMarker = MKPointAnnotation() + private var isAnimatingMap = false private var mapRegionAnimationDidEnd: (() -> Void)? override var preferredStatusBarStyle: UIStatusBarStyle { @@ -82,12 +83,12 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen override func viewDidLoad() { super.viewDidLoad() - mainContentView.connectButton.addTarget(self, action: #selector(handleConnect(_:)), for: .touchUpInside) - mainContentView.cancelButton.addTarget(self, action: #selector(handleDisconnect(_:)), for: .touchUpInside) - mainContentView.splitDisconnectButton.primaryButton.addTarget(self, action: #selector(handleDisconnect(_:)), for: .touchUpInside) - mainContentView.splitDisconnectButton.secondaryButton.addTarget(self, action: #selector(handleReconnect(_:)), for: .touchUpInside) + contentView.connectButton.addTarget(self, action: #selector(handleConnect(_:)), for: .touchUpInside) + contentView.cancelButton.addTarget(self, action: #selector(handleDisconnect(_:)), for: .touchUpInside) + contentView.splitDisconnectButton.primaryButton.addTarget(self, action: #selector(handleDisconnect(_:)), for: .touchUpInside) + contentView.splitDisconnectButton.secondaryButton.addTarget(self, action: #selector(handleReconnect(_:)), for: .touchUpInside) - mainContentView.selectLocationButton.addTarget(self, action: #selector(handleSelectLocation(_:)), for: .touchUpInside) + contentView.selectLocationButton.addTarget(self, action: #selector(handleSelectLocation(_:)), for: .touchUpInside) TunnelManager.shared.addObserver(self) self.tunnelState = TunnelManager.shared.tunnelState @@ -111,7 +112,7 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen func setMainContentHidden(_ isHidden: Bool, animated: Bool) { let actions = { - self.mainContentView.containerView.alpha = isHidden ? 0 : 1 + self.contentView.containerView.alpha = isHidden ? 0 : 1 } if animated { @@ -122,13 +123,25 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen } private func addSubviews() { - view.addSubview(mainContentView) + view.addSubview(contentView) + NSLayoutConstraint.activate([ - mainContentView.topAnchor.constraint(equalTo: view.topAnchor), - mainContentView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - mainContentView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - mainContentView.bottomAnchor.constraint(equalTo: view.bottomAnchor) + contentView.topAnchor.constraint(equalTo: view.topAnchor), + contentView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + contentView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + contentView.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) + + // Force layout since we rely on view frames when positioning map camera. + view.layoutIfNeeded() + } + + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + + coordinator.animate(alongsideTransition: { _ in }, completion: { context in + self.updateLocation(animated: context.isAnimated) + }) } // MARK: - TunnelObserver @@ -152,10 +165,10 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen // MARK: - Private private func updateUserInterfaceForTunnelStateChange() { - mainContentView.secureLabel.text = tunnelState.localizedTitleForSecureLabel.uppercased() - mainContentView.secureLabel.textColor = tunnelState.textColorForSecureLabel + contentView.secureLabel.text = tunnelState.localizedTitleForSecureLabel.uppercased() + contentView.secureLabel.textColor = tunnelState.textColorForSecureLabel - mainContentView.connectButton.setTitle( + contentView.connectButton.setTitle( NSLocalizedString( "CONNECT_BUTTON_TITLE", tableName: "Main", @@ -163,15 +176,15 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen comment: "" ), for: .normal ) - mainContentView.selectLocationButton.setTitle(tunnelState.localizedTitleForSelectLocationButton, for: .normal) - mainContentView.cancelButton.setTitle( + contentView.selectLocationButton.setTitle(tunnelState.localizedTitleForSelectLocationButton, for: .normal) + contentView.cancelButton.setTitle( NSLocalizedString( "CANCEL_BUTTON_TITLE", tableName: "Main", value: "Cancel", comment: "" ), for: .normal) - mainContentView.splitDisconnectButton.primaryButton.setTitle( + contentView.splitDisconnectButton.primaryButton.setTitle( NSLocalizedString( "DISCONNECT_BUTTON_TITLE", tableName: "Main", @@ -179,7 +192,7 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen comment: "" ), for: .normal ) - mainContentView.splitDisconnectButton.secondaryButton.accessibilityLabel = NSLocalizedString( + contentView.splitDisconnectButton.secondaryButton.accessibilityLabel = NSLocalizedString( "RECONNECT_BUTTON_ACCESSIBILITY_LABEL", tableName: "Main", value: "Reconnect", @@ -190,7 +203,7 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen } private func updateTraitDependentViews() { - mainContentView.setActionButtons(tunnelState.actionButtons(traitCollection: self.traitCollection)) + contentView.setActionButtons(tunnelState.actionButtons(traitCollection: self.traitCollection)) } private func attributedStringForLocation(string: String) -> NSAttributedString { @@ -213,56 +226,54 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen setTunnelRelay(nil) } - mainContentView.locationContainerView.accessibilityLabel = tunnelState.localizedAccessibilityLabel + contentView.locationContainerView.accessibilityLabel = tunnelState.localizedAccessibilityLabel } private func setTunnelRelay(_ tunnelRelay: PacketTunnelRelay?) { if let tunnelRelay = tunnelRelay { - mainContentView.cityLabel.attributedText = attributedStringForLocation(string: tunnelRelay.location.city) - mainContentView.countryLabel.attributedText = attributedStringForLocation(string: tunnelRelay.location.country) + contentView.cityLabel.attributedText = attributedStringForLocation(string: tunnelRelay.location.city) + contentView.countryLabel.attributedText = attributedStringForLocation(string: tunnelRelay.location.country) - mainContentView.connectionPanel.dataSource = ConnectionPanelData( + contentView.connectionPanel.dataSource = ConnectionPanelData( inAddress: "\(tunnelRelay.ipv4Relay) UDP", outAddress: nil ) - mainContentView.connectionPanel.isHidden = false - mainContentView.connectionPanel.connectedRelayName = tunnelRelay.hostname + contentView.connectionPanel.isHidden = false + contentView.connectionPanel.connectedRelayName = tunnelRelay.hostname } else { - mainContentView.countryLabel.attributedText = attributedStringForLocation(string: " ") - mainContentView.cityLabel.attributedText = attributedStringForLocation(string: " ") - mainContentView.connectionPanel.dataSource = nil - mainContentView.connectionPanel.isHidden = true + contentView.countryLabel.attributedText = attributedStringForLocation(string: " ") + contentView.cityLabel.attributedText = attributedStringForLocation(string: " ") + contentView.connectionPanel.dataSource = nil + contentView.connectionPanel.isHidden = true } } private func locationMarkerOffset() -> CGPoint { // Compute the activity indicator frame within the view coordinate system. - let activityIndicatorFrame = mainContentView.activityIndicator.convert(mainContentView.activityIndicator.bounds, to: view) + let activityIndicatorFrame = contentView.activityIndicator.convert(contentView.activityIndicator.bounds, to: view) // Compute the offset to align the marker on the map with activity indicator. - let offsetY = activityIndicatorFrame.midY - mainContentView.mapView.frame.midY + let offsetY = activityIndicatorFrame.midY - contentView.mapView.frame.midY return CGPoint(x: 0, y: offsetY) } - private func computeCoordinateRegion(centerCoordinate: CLLocationCoordinate2D, centerOffsetInPoints: CGPoint) -> MKCoordinateRegion { + private func computeCoordinateRegion(center: CLLocationCoordinate2D, offset: CGPoint) -> MKCoordinateRegion { let span = MKCoordinateSpan(latitudeDelta: 30, longitudeDelta: 30) - var region = MKCoordinateRegion(center: centerCoordinate, span: span) - region = mainContentView.mapView.regionThatFits(region) + var region = contentView.mapView.regionThatFits(MKCoordinateRegion(center: center, span: span)) - let latitudeDeltaPerPoint = region.span.latitudeDelta / Double(mainContentView.mapView.frame.height) - var offsetCenter = centerCoordinate - offsetCenter.latitude += CLLocationDegrees(latitudeDeltaPerPoint * Double(centerOffsetInPoints.y)) - region.center = offsetCenter + let latitudeDeltaPerPoint = region.span.latitudeDelta / contentView.mapView.frame.height + region.center = center + region.center.latitude += CLLocationDegrees(latitudeDeltaPerPoint * offset.y) - return region + return contentView.mapView.regionThatFits(region) } private func updateLocation(animated: Bool) { switch tunnelState { case .connecting(let tunnelRelay): removeLocationMarker() - mainContentView.activityIndicator.startAnimating() + contentView.activityIndicator.startAnimating() if let tunnelRelay = tunnelRelay { setLocation(coordinate: tunnelRelay.location.geoCoordinate, animated: animated) @@ -272,23 +283,33 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen case .reconnecting(let tunnelRelay): removeLocationMarker() - mainContentView.activityIndicator.startAnimating() + contentView.activityIndicator.startAnimating() setLocation(coordinate: tunnelRelay.location.geoCoordinate, animated: animated) case .connected(let tunnelRelay): + // Show marker right away if activity indicator is not animating, i.e when the app + // launches with connected tunnel. + let showMarkerRightAway = !contentView.activityIndicator.isAnimating + + if showMarkerRightAway { + addLocationMarker(coordinate: tunnelRelay.location.geoCoordinate) + } + setLocation(coordinate: tunnelRelay.location.geoCoordinate, animated: animated) { [weak self] in - self?.mainContentView.activityIndicator.stopAnimating() - self?.addLocationMarker(coordinate: tunnelRelay.location.geoCoordinate) + if !showMarkerRightAway { + self?.contentView.activityIndicator.stopAnimating() + self?.addLocationMarker(coordinate: tunnelRelay.location.geoCoordinate) + } } case .pendingReconnect: removeLocationMarker() - mainContentView.activityIndicator.startAnimating() + contentView.activityIndicator.startAnimating() case .disconnected, .disconnecting: removeLocationMarker() - mainContentView.activityIndicator.stopAnimating() + contentView.activityIndicator.stopAnimating() unsetLocation(animated: animated) } @@ -296,45 +317,50 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen private func addLocationMarker(coordinate: CLLocationCoordinate2D) { locationMarker.coordinate = coordinate - mainContentView.mapView.addAnnotation(locationMarker) + contentView.mapView.addAnnotation(locationMarker) } private func removeLocationMarker() { - mainContentView.mapView.removeAnnotation(locationMarker) + contentView.mapView.removeAnnotation(locationMarker) } private func setLocation(coordinate: CLLocationCoordinate2D, animated: Bool, animationDidEnd: (() -> Void)? = nil) { - if let lastLocation = lastLocation, coordinate.approximatelyEqualTo(lastLocation) { - mapRegionAnimationDidEnd = nil - animationDidEnd?() + let markerOffset = locationMarkerOffset() + let region = computeCoordinateRegion(center: coordinate, offset: markerOffset) + + if targetRegion?.isApproximatelyEqualTo(region) ?? false { + if isAnimatingMap { + mapRegionAnimationDidEnd = animationDidEnd + } else { + animationDidEnd?() + } return } mapRegionAnimationDidEnd = animationDidEnd - - let markerOffset = locationMarkerOffset() - let region = computeCoordinateRegion(centerCoordinate: coordinate, centerOffsetInPoints: markerOffset) - - mainContentView.mapView.setRegion(region, animated: animated) - - self.lastLocation = coordinate + setMapRegion(region, animated: animated) } - private func unsetLocation(animated: Bool, animationDidEnd: (() -> Void)? = nil) { + private func unsetLocation(animated: Bool) { + let span = MKCoordinateSpan(latitudeDelta: 90, longitudeDelta: 90) let coordinate = CLLocationCoordinate2D(latitude: 0, longitude: 0) - if let lastLocation = lastLocation, coordinate.approximatelyEqualTo(lastLocation) { - mapRegionAnimationDidEnd = nil - animationDidEnd?() + let region = contentView.mapView.regionThatFits( + MKCoordinateRegion(center: coordinate, span: span) + ) + + mapRegionAnimationDidEnd = nil + + if targetRegion?.isApproximatelyEqualTo(region) ?? false { return } - mapRegionAnimationDidEnd = animationDidEnd - - let span = MKCoordinateSpan(latitudeDelta: 90, longitudeDelta: 90) - let region = MKCoordinateRegion(center: coordinate, span: span) - mainContentView.mapView.setRegion(region, animated: animated) + setMapRegion(region, animated: animated) + } - self.lastLocation = coordinate + private func setMapRegion(_ region: MKCoordinateRegion, animated: Bool) { + contentView.mapView.setRegion(region, animated: animated) + isAnimatingMap = true + targetRegion = region } private func addNotificationController() { @@ -406,6 +432,7 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) { mapRegionAnimationDidEnd?() mapRegionAnimationDidEnd = nil + isAnimatingMap = false } // MARK: - Private @@ -415,13 +442,13 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen } private func setupMapView() { - mainContentView.mapView.insetsLayoutMarginsFromSafeArea = false - mainContentView.mapView.delegate = self - mainContentView.mapView.register(MKAnnotationView.self, forAnnotationViewWithReuseIdentifier: "location") + contentView.mapView.insetsLayoutMarginsFromSafeArea = false + contentView.mapView.delegate = self + contentView.mapView.register(MKAnnotationView.self, forAnnotationViewWithReuseIdentifier: "location") if #available(iOS 13.0, *) { // Use dark style for the map to dim the map grid - mainContentView.mapView.overrideUserInterfaceStyle = .dark + contentView.mapView.overrideUserInterfaceStyle = .dark } addTileOverlay() @@ -436,7 +463,7 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen // Replace the default map tiles tileOverlay.canReplaceMapContent = true - mainContentView.mapView.addOverlay(tileOverlay) + contentView.mapView.addOverlay(tileOverlay) } private func loadGeoJSONData() { @@ -452,7 +479,7 @@ class ConnectViewController: UIViewController, MKMapViewDelegate, RootContainmen let data = try Data(contentsOf: fileURL) let overlays = try GeoJSON.decodeGeoJSON(data) - mainContentView.mapView.addOverlays(overlays) + contentView.mapView.addOverlays(overlays) } catch { logger.error(chainedError: AnyChainedError(error), message: "Failed to load geojson.") } @@ -604,7 +631,7 @@ private extension TunnelState { } } - func actionButtons(traitCollection: UITraitCollection) -> [ConnectMainContentView.ActionButton] { + func actionButtons(traitCollection: UITraitCollection) -> [ConnectContentView.ActionButton] { switch (traitCollection.userInterfaceIdiom, traitCollection.horizontalSizeClass) { case (.phone, _), (.pad, .compact): switch self { @@ -637,9 +664,12 @@ private extension TunnelState { } -private extension CLLocationCoordinate2D { - func approximatelyEqualTo(_ other: CLLocationCoordinate2D) -> Bool { - return fabs(self.latitude - other.latitude) <= .ulpOfOne && - fabs(self.longitude - other.longitude) <= .ulpOfOne +private extension MKCoordinateRegion { + func isApproximatelyEqualTo(_ other: MKCoordinateRegion) -> Bool { + return fabs(center.latitude - other.center.latitude) <= .ulpOfOne && + fabs(center.longitude - other.center.longitude) <= .ulpOfOne && + fabs(span.latitudeDelta - other.span.latitudeDelta) <= .ulpOfOne && + fabs(span.longitudeDelta - other.span.longitudeDelta) <= .ulpOfOne } + } |
