Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clean up StyleManager and respect dynamic type #65

Merged
merged 3 commits into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
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 Expand Up @@ -264,7 +268,7 @@

var maneuvers: [CPManeuver] = [primaryManeuver]

// Add tertiary text if available. TODO: handle lanes.

Check warning on line 271 in MapboxNavigation/CarPlayNavigationViewController.swift

View workflow job for this annotation

GitHub Actions / Code Style

TODOs should be resolved (handle lanes.) (todo)
if let tertiaryInstruction = visualInstruction.tertiaryInstruction, !tertiaryInstruction.containsLaneIndications {
let tertiaryManeuver = CPManeuver()
tertiaryManeuver.symbolSet = tertiaryInstruction.maneuverImageSet(side: visualInstruction.drivingSide)
Expand All @@ -291,7 +295,7 @@

func endOfRouteFeedbackTemplate() -> CPGridTemplate {
let buttonHandler: (_: CPGridButton) -> Void = { [weak self] _ in
// TODO: no such method exists, and the replacement candidate ignores the feedback sent, so ... ?

Check warning on line 298 in MapboxNavigation/CarPlayNavigationViewController.swift

View workflow job for this annotation

GitHub Actions / Code Style

TODOs should be resolved (no such method exists, and the...) (todo)
// self?.routeController.setEndOfRoute(rating: Int(button.titleVariants.first!.components(separatedBy: CharacterSet.decimalDigits.inverted).joined())!, comment: nil)
self?.carInterfaceController.popTemplate(animated: true)
self?.exitNavigation()
Expand Down
21 changes: 15 additions & 6 deletions MapboxNavigation/NavigationViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -391,9 +391,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 @@ -419,7 +418,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, routeController: RouteController? = nil, locationManager: NavigationLocationManager? = nil) {
Expand Down Expand Up @@ -651,8 +655,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()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch to cancel this when one is in a tunnel. But don't we need to re-add the timer once the user leaves the tunnel?

Copy link
Collaborator Author

@michaelkirk michaelkirk Jul 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair question! I should have called this out at the top level, but this is actually not a change in behavior. Previously this logic lived in applyStyle.

I was working on applyStyle and could not figure out why we'd cancel the timer. Eventually I realized it was only for this case, so I extracted the functionality here.

After you exit the tunnel, same as before, self.styleManager.timeOfDayChanged() is called which restarts the timer.

e.g. the green area roughly corresponds to a tunnel over the highway:

tunnel.mov.mp4

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A little more context in: f8eb874

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
Loading