diff --git a/MapboxNavigation/CarPlayMapViewController.swift b/MapboxNavigation/CarPlayMapViewController.swift index 0feff433..83f04ac1 100644 --- a/MapboxNavigation/CarPlayMapViewController.swift +++ b/MapboxNavigation/CarPlayMapViewController.swift @@ -49,13 +49,17 @@ class CarPlayMapViewController: UIViewController, MLNMapViewDelegate { override func viewDidLoad() { super.viewDidLoad() - self.styleManager = StyleManager(self) - self.styleManager.styles = [DayStyle(demoStyle: ()), NightStyle(demoStyle: ())] + self.styleManager = StyleManager(self, dayStyle: DayStyle(demoStyle: ()), nightStyle: NightStyle(demoStyle: ())) self.resetCamera(animated: false, altitude: CarPlayMapViewController.defaultAltitude) self.mapView.setUserTrackingMode(.followWithCourse, animated: true, completionHandler: nil) } - + + override open func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + self.styleManager.ensureAppropriateStyle() + } + public func zoomInButton() -> CPMapButton { let zoomInButton = CPMapButton { [weak self] _ in guard let strongSelf = self else { diff --git a/MapboxNavigation/CarPlayNavigationViewController.swift b/MapboxNavigation/CarPlayNavigationViewController.swift index e0207014..cf4365bd 100644 --- a/MapboxNavigation/CarPlayNavigationViewController.swift +++ b/MapboxNavigation/CarPlayNavigationViewController.swift @@ -84,14 +84,18 @@ public class CarPlayNavigationViewController: UIViewController, MLNMapViewDelega self.mapView = mapView view.addSubview(mapView) - self.styleManager = StyleManager(self) - self.styleManager.styles = [DayStyle(demoStyle: ()), NightStyle(demoStyle: ())] + self.styleManager = StyleManager(self, dayStyle: DayStyle(demoStyle: ()), nightStyle: NightStyle(demoStyle: ())) self.resumeNotifications() self.routeController.resume() mapView.recenterMap() } - + + override open func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + self.styleManager.ensureAppropriateStyle() + } + override public func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) self.suspendNotifications() diff --git a/MapboxNavigation/NavigationViewController.swift b/MapboxNavigation/NavigationViewController.swift index 3195d941..c370b661 100644 --- a/MapboxNavigation/NavigationViewController.swift +++ b/MapboxNavigation/NavigationViewController.swift @@ -404,9 +404,8 @@ open class NavigationViewController: UIViewController { self.view.addSubview(mapSubview) mapSubview.pinInSuperview() - self.styleManager = StyleManager(self) - self.styleManager.styles = [dayStyle, nightStyle] - + self.styleManager = StyleManager(self, dayStyle: dayStyle, nightStyle: nightStyle) + self.mapViewController.navigationView.hideUI(animated: false) self.mapView.tracksUserCourse = false } @@ -432,7 +431,12 @@ open class NavigationViewController: UIViewController { self.resumeNotifications() self.view.clipsToBounds = true } - + + override open func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + self.styleManager.ensureAppropriateStyle() + } + // MARK: - NavigationViewController public func startNavigation(with route: Route, animated: Bool, routeController: RouteController? = nil, locationManager: NavigationLocationManager = NavigationLocationManager()) { @@ -669,8 +673,13 @@ extension NavigationViewController: RouteControllerDelegate { extension NavigationViewController: TunnelIntersectionManagerDelegate { public func tunnelIntersectionManager(_ manager: TunnelIntersectionManager, willEnableAnimationAt location: CLLocation) { - self.routeController?.tunnelIntersectionManager(manager, willEnableAnimationAt: location) - self.styleManager.applyStyle(type: .night) + guard let routeController else { + return + } + routeController.tunnelIntersectionManager(manager, willEnableAnimationAt: location) + // If we're in a tunnel at sunrise, don't let the timeOfDay timer clobber night mode + self.styleManager.cancelTimeOfDayTimer() + self.styleManager.ensureStyle(type: .night) } public func tunnelIntersectionManager(_ manager: TunnelIntersectionManager, willDisableAnimationAt location: CLLocation) { diff --git a/MapboxNavigation/StyleManager.swift b/MapboxNavigation/StyleManager.swift index 5e83c62c..1a681d02 100644 --- a/MapboxNavigation/StyleManager.swift +++ b/MapboxNavigation/StyleManager.swift @@ -37,40 +37,48 @@ open class StyleManager: NSObject { /** Determines whether the style manager should apply a new style given the time of day. - - precondition: Two styles must be provided for this property to have any effect. + - precondition: `nightStyle` must be provided for this property to have any effect. */ @objc public var automaticallyAdjustsStyleForTimeOfDay = true { didSet { + assert(!self.automaticallyAdjustsStyleForTimeOfDay || self.nightStyle != nil, "`nightStyle` must be specified in order to adjust style for time of day") self.resetTimeOfDayTimer() } } - - /** - The styles that are in circulation. Active style is set based on - the sunrise and sunset at your current location. A change of - preferred content size by the user will also trigger an update. - - - precondition: Two styles must be provided for - `StyleManager.automaticallyAdjustsStyleForTimeOfDay` to have any effect. - */ - @objc public var styles = [Style]() { + + /// Useful for testing + var stubbedDate: Date? + + var currentStyleAndSize: (Style, UIContentSizeCategory)? + + /// The style used from sunrise to sunset. + /// + /// If `nightStyle` is nil, `dayStyle` will be used for all times. + @objc public var dayStyle: Style { + didSet { + self.ensureAppropriateStyle() + } + } + + /// The style used from sunset to sunrise. + /// + /// If `nightStyle` is nil, `dayStyle` will be used for all times. + @objc public var nightStyle: Style? { didSet { - self.applyStyle() self.resetTimeOfDayTimer() + self.ensureAppropriateStyle() } } - - var date: Date? - - var currentStyleType: StyleType? - + /** Initializes a new `StyleManager`. - parameter delegate: The receiver’s delegate */ - public required init(_ delegate: StyleManagerDelegate) { + public required init(_ delegate: StyleManagerDelegate, dayStyle: Style, nightStyle: Style? = nil) { self.delegate = delegate + self.dayStyle = dayStyle + self.nightStyle = nightStyle super.init() self.resumeNotifications() self.resetTimeOfDayTimer() @@ -94,10 +102,10 @@ open class StyleManager: NSObject { func resetTimeOfDayTimer() { NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(self.timeOfDayChanged), object: nil) - guard self.automaticallyAdjustsStyleForTimeOfDay, self.styles.count > 1 else { return } + guard self.automaticallyAdjustsStyleForTimeOfDay, self.nightStyle != nil else { return } guard let location = delegate?.locationFor(styleManager: self) else { return } - guard let solar = Solar(date: date, coordinate: location.coordinate), + guard let solar = Solar(date: stubbedDate, coordinate: location.coordinate), let sunrise = solar.sunrise, let sunset = solar.sunset else { return @@ -110,57 +118,59 @@ open class StyleManager: NSObject { perform(#selector(self.timeOfDayChanged), with: nil, afterDelay: interval + 1) } - + @objc func preferredContentSizeChanged(_ notification: Notification) { - self.applyStyle() + self.ensureAppropriateStyle() } - + + /// Useful when you don't want the time of day to change the style. For example if you're in a tunnel. + @objc func cancelTimeOfDayTimer() { + NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(self.timeOfDayChanged), object: nil) + } + @objc func timeOfDayChanged() { - self.forceRefreshAppearanceIfNeeded() + self.ensureAppropriateStyle() self.resetTimeOfDayTimer() } - func applyStyle(type styleType: StyleType) { - guard self.currentStyleType != styleType else { return } - - NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(self.timeOfDayChanged), object: nil) - - for style in self.styles where style.styleType == styleType { - style.apply() - currentStyleType = styleType - delegate?.styleManager?(self, didApply: style) + func ensureAppropriateStyle() { + guard self.nightStyle != nil else { + self.ensureStyle(style: self.dayStyle) + return } - - self.forceRefreshAppearance() - } - - func applyStyle() { + guard let location = delegate?.locationFor(styleManager: self) else { // We can't calculate sunset or sunrise w/o a location so just apply the first style - if let style = styles.first, currentStyleType != style.styleType { - self.currentStyleType = style.styleType - style.apply() - self.delegate?.styleManager?(self, didApply: style) - } + self.ensureStyle(style: self.dayStyle) return } - - // Single style usage - guard self.styles.count > 1 else { - if let style = styles.first, currentStyleType != style.styleType { - self.currentStyleType = style.styleType - style.apply() - self.delegate?.styleManager?(self, didApply: style) - } + + self.ensureStyle(type: self.styleType(for: location)) + } + + func ensureStyle(type: StyleType) { + switch type { + case .day: + self.ensureStyle(style: self.dayStyle) + case .night: + self.ensureStyle(style: self.nightStyle ?? self.dayStyle) + } + } + + func ensureStyle(style: Style) { + let preferredContentSizeCategory = UIApplication.shared.preferredContentSizeCategory + + if let currentStyleAndSize, currentStyleAndSize == (style, preferredContentSizeCategory) { return } - - let styleTypeForTimeOfDay = self.styleType(for: location) - self.applyStyle(type: styleTypeForTimeOfDay) + self.currentStyleAndSize = (style, preferredContentSizeCategory) + style.apply() + self.delegate?.styleManager?(self, didApply: style) + self.refreshAppearance() } - + func styleType(for location: CLLocation) -> StyleType { - guard let solar = Solar(date: date, coordinate: location.coordinate), + guard let solar = Solar(date: stubbedDate, coordinate: location.coordinate), let sunrise = solar.sunrise, let sunset = solar.sunset else { return .day @@ -169,24 +179,7 @@ open class StyleManager: NSObject { return solar.date.isNighttime(sunrise: sunrise, sunset: sunset) ? .night : .day } - func forceRefreshAppearanceIfNeeded() { - guard let location = delegate?.locationFor(styleManager: self) else { return } - - let styleTypeForLocation = self.styleType(for: location) - - // If `styles` does not contain at least one style for the selected location, don't try and apply it. - let availableStyleTypesForLocation = self.styles.filter { $0.styleType == styleTypeForLocation } - guard availableStyleTypesForLocation.count > 0 else { return } - - guard self.currentStyleType != styleTypeForLocation else { - return - } - - self.applyStyle() - self.forceRefreshAppearance() - } - - func forceRefreshAppearance() { + func refreshAppearance() { for window in UIApplication.shared.windows { for view in window.subviews { view.removeFromSuperview() diff --git a/MapboxNavigationTests/Sources/Tests/StyleManagerTests.swift b/MapboxNavigationTests/Sources/Tests/StyleManagerTests.swift index a6d0a1c8..93a92899 100644 --- a/MapboxNavigationTests/Sources/Tests/StyleManagerTests.swift +++ b/MapboxNavigationTests/Sources/Tests/StyleManagerTests.swift @@ -15,7 +15,7 @@ class StyleManagerTests: XCTestCase { override func setUp() { super.setUp() - self.styleManager = StyleManager(self) + self.styleManager = StyleManager(self, dayStyle: DayStyle(demoStyle: ()), nightStyle: NightStyle(demoStyle: ())) self.styleManager.automaticallyAdjustsStyleForTimeOfDay = true } @@ -32,17 +32,17 @@ class StyleManagerTests: XCTestCase { let afterSunset = dateFormatter.date(from: "21:00")! let midnight = dateFormatter.date(from: "00:00")! - self.styleManager.date = beforeSunrise + self.styleManager.stubbedDate = beforeSunrise XCTAssert(self.styleManager.styleType(for: self.location) == .night) - self.styleManager.date = afterSunrise + self.styleManager.stubbedDate = afterSunrise XCTAssert(self.styleManager.styleType(for: self.location) == .day) - self.styleManager.date = noonDate + self.styleManager.stubbedDate = noonDate XCTAssert(self.styleManager.styleType(for: self.location) == .day) - self.styleManager.date = beforeSunset + self.styleManager.stubbedDate = beforeSunset XCTAssert(self.styleManager.styleType(for: self.location) == .day) - self.styleManager.date = afterSunset + self.styleManager.stubbedDate = afterSunset XCTAssert(self.styleManager.styleType(for: self.location) == .night) - self.styleManager.date = midnight + self.styleManager.stubbedDate = midnight XCTAssert(self.styleManager.styleType(for: self.location) == .night) } @@ -61,17 +61,17 @@ class StyleManagerTests: XCTestCase { let justAfterSunset = dateFormatter.date(from: "17:04:30")! let midnight = dateFormatter.date(from: "00:00:00")! - self.styleManager.date = justBeforeSunrise + self.styleManager.stubbedDate = justBeforeSunrise XCTAssert(self.styleManager.styleType(for: self.location) == .night) - self.styleManager.date = justAfterSunrise + self.styleManager.stubbedDate = justAfterSunrise XCTAssert(self.styleManager.styleType(for: self.location) == .day) - self.styleManager.date = noonDate + self.styleManager.stubbedDate = noonDate XCTAssert(self.styleManager.styleType(for: self.location) == .day) - self.styleManager.date = juetBeforeSunset + self.styleManager.stubbedDate = juetBeforeSunset XCTAssert(self.styleManager.styleType(for: self.location) == .day) - self.styleManager.date = justAfterSunset + self.styleManager.stubbedDate = justAfterSunset XCTAssert(self.styleManager.styleType(for: self.location) == .night) - self.styleManager.date = midnight + self.styleManager.stubbedDate = midnight XCTAssert(self.styleManager.styleType(for: self.location) == .night) } @@ -91,17 +91,17 @@ class StyleManagerTests: XCTestCase { let afterSunset = dateFormatter.date(from: "09:00 PM")! let midnight = dateFormatter.date(from: "00:00 AM")! - self.styleManager.date = beforeSunrise + self.styleManager.stubbedDate = beforeSunrise XCTAssert(self.styleManager.styleType(for: self.location) == .night) - self.styleManager.date = afterSunrise + self.styleManager.stubbedDate = afterSunrise XCTAssert(self.styleManager.styleType(for: self.location) == .day) - self.styleManager.date = noonDate + self.styleManager.stubbedDate = noonDate XCTAssert(self.styleManager.styleType(for: self.location) == .day) - self.styleManager.date = beforeSunset + self.styleManager.stubbedDate = beforeSunset XCTAssert(self.styleManager.styleType(for: self.location) == .day) - self.styleManager.date = afterSunset + self.styleManager.stubbedDate = afterSunset XCTAssert(self.styleManager.styleType(for: self.location) == .night) - self.styleManager.date = midnight + self.styleManager.stubbedDate = midnight XCTAssert(self.styleManager.styleType(for: self.location) == .night) }