Skip to content

Commit

Permalink
Clean up StyleManager and respect dynamic type (#65)
Browse files Browse the repository at this point in the history
* refactor StyleManager to explicit day/night styles

As I understand it, we support 1 or 2 styles, but never more than 2.
Keeping styles in a potentially empty/boundless Array made the internals
of this class more complicated than necessary.

This is intended to be a no-op from a user facing perspective, though
the API has changed and there may have been times before where
`refreshAppearence` was called twice, where now we'll only call it once.

Also changed:
 - Deduped some of "apply appropriate style" logic.
 - Better labeled the intent of tunnel style logic. Previously it took
   me a while to understand why were canceling the timer in
   `applyStyleType` (we dont want to unset .night theme if we happen to be in a tunnel at sunrise)

Note: there remains a pre-existing bug: `preferredContentSizeChanged`
will not re-apply until the style changes for some other reasons (like entering a tunnel or time
of day change).

* clearer name

* Navigation UI respects text size preference
  • Loading branch information
michaelkirk authored Jul 22, 2024
1 parent 0d439d2 commit 5143a0f
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 105 deletions.
10 changes: 7 additions & 3 deletions MapboxNavigation/CarPlayMapViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
10 changes: 7 additions & 3 deletions MapboxNavigation/CarPlayNavigationViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
21 changes: 15 additions & 6 deletions MapboxNavigation/NavigationViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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()) {
Expand Down Expand Up @@ -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) {
Expand Down
141 changes: 67 additions & 74 deletions MapboxNavigation/StyleManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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()
Expand Down
38 changes: 19 additions & 19 deletions MapboxNavigationTests/Sources/Tests/StyleManagerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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)
}

Expand All @@ -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)
}

Expand All @@ -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)
}

Expand Down

0 comments on commit 5143a0f

Please sign in to comment.