From 4fd6d771da8a28f7c5452958040b9c10d15c5e11 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Thu, 13 Jun 2024 19:24:43 -0700 Subject: [PATCH] FIX crashing EORVC FIXES #57 - by rewriting EORVC as programmatic VC rather than storyboard. FIXES #62 - Omitted the "Rate this route" feedback while rewriting the EORVC. Note this was cherry-picked, and no longer works due to change in #54 --- MapboxCoreNavigation/EndOfRouteFeedback.swift | 27 --- MapboxNavigation/DayStyle.swift | 6 - .../EndOfRouteViewController.swift | 219 +++++++----------- MapboxNavigation/RatingControl.swift | 136 ----------- .../Base.lproj/Navigation.storyboard | 200 ---------------- .../Resources/Base.lproj/Navigation.strings | 3 - MapboxNavigation/RouteMapViewController.swift | 16 +- MapboxNavigation/UIView.swift | 5 +- 8 files changed, 93 insertions(+), 519 deletions(-) delete mode 100644 MapboxCoreNavigation/EndOfRouteFeedback.swift delete mode 100644 MapboxNavigation/RatingControl.swift delete mode 100644 MapboxNavigation/Resources/Base.lproj/Navigation.storyboard delete mode 100644 MapboxNavigation/Resources/Base.lproj/Navigation.strings diff --git a/MapboxCoreNavigation/EndOfRouteFeedback.swift b/MapboxCoreNavigation/EndOfRouteFeedback.swift deleted file mode 100644 index 54c3e5cb5..000000000 --- a/MapboxCoreNavigation/EndOfRouteFeedback.swift +++ /dev/null @@ -1,27 +0,0 @@ -import Foundation - -/** - Feedback Model Object for End Of Route Experience. - */ -@objc open class EndOfRouteFeedback: NSObject { - /** - Rating: The user's rating for the route. Normalized between 0 and 100. - */ - let rating: Int? - - /** - Comment: Any comments that the user had about the route. - */ - let comment: String? - - @nonobjc public init(rating: Int? = nil, comment: String? = nil) { - self.rating = rating - self.comment = comment - super.init() - } - - @objc public convenience init(rating ratingNumber: NSNumber?, comment: String?) { - let rating = ratingNumber?.intValue - self.init(rating: rating, comment: comment) - } -} diff --git a/MapboxNavigation/DayStyle.swift b/MapboxNavigation/DayStyle.swift index d0f6dc555..b8113ef96 100644 --- a/MapboxNavigation/DayStyle.swift +++ b/MapboxNavigation/DayStyle.swift @@ -129,8 +129,6 @@ open class DayStyle: Style { PrimaryLabel.appearance(whenContainedInInstancesOf: [InstructionsBannerView.self]).normalTextColor = #colorLiteral(red: 0.09803921569, green: 0.09803921569, blue: 0.09803921569, alpha: 1) PrimaryLabel.appearance(whenContainedInInstancesOf: [StepInstructionsView.self]).normalTextColor = #colorLiteral(red: 0.09803921569, green: 0.09803921569, blue: 0.09803921569, alpha: 1) ProgressBar.appearance().barColor = #colorLiteral(red: 0.149, green: 0.239, blue: 0.341, alpha: 1) - RatingControl.appearance().normalColor = #colorLiteral(red: 0.8508961797, green: 0.8510394692, blue: 0.850877285, alpha: 1) - RatingControl.appearance().selectedColor = #colorLiteral(red: 0.1205472574, green: 0.2422055006, blue: 0.3489340544, alpha: 1) ReportButton.appearance().backgroundColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1) ReportButton.appearance().textColor = tintColor! ReportButton.appearance().textFont = UIFont.systemFont(ofSize: 15, weight: .medium).adjustedFont @@ -189,8 +187,6 @@ open class NightStyle: DayStyle { DistanceLabel.appearance(whenContainedInInstancesOf: [StepInstructionsView.self]).valueTextColor = #colorLiteral(red: 0.9842069745, green: 0.9843751788, blue: 0.9841964841, alpha: 1) DistanceRemainingLabel.appearance().normalTextColor = #colorLiteral(red: 0.7991961837, green: 0.8232284188, blue: 0.8481693864, alpha: 1) EndOfRouteButton.appearance().textColor = .white - EndOfRouteCommentView.appearance().backgroundColor = #colorLiteral(red: 0.1875049942, green: 0.2981707989, blue: 0.4181857639, alpha: 1) - EndOfRouteCommentView.appearance().normalTextColor = .white EndOfRouteContentView.appearance().backgroundColor = backgroundColor EndOfRouteStaticLabel.appearance().alpha = 1.0 EndOfRouteStaticLabel.appearance().textColor = UIColor.white.withAlphaComponent(0.9) @@ -216,8 +212,6 @@ open class NightStyle: DayStyle { PrimaryLabel.appearance(whenContainedInInstancesOf: [InstructionsBannerView.self]).normalTextColor = #colorLiteral(red: 0.9996390939, green: 1, blue: 0.9997561574, alpha: 1) PrimaryLabel.appearance(whenContainedInInstancesOf: [StepInstructionsView.self]).normalTextColor = #colorLiteral(red: 0.9996390939, green: 1, blue: 0.9997561574, alpha: 1) ProgressBar.appearance().barColor = #colorLiteral(red: 0.9842069745, green: 0.9843751788, blue: 0.9841964841, alpha: 1) - RatingControl.appearance().normalColor = #colorLiteral(red: 0.149668334, green: 0.1680230035, blue: 0.1472480238, alpha: 1) - RatingControl.appearance().selectedColor = #colorLiteral(red: 0.9803059896, green: 0.9978019022, blue: 1, alpha: 1) ReportButton.appearance().backgroundColor = backgroundColor ReportButton.appearance().textColor = #colorLiteral(red: 0.9842069745, green: 0.9843751788, blue: 0.9841964841, alpha: 1) ResumeButton.appearance().backgroundColor = backgroundColor diff --git a/MapboxNavigation/EndOfRouteViewController.swift b/MapboxNavigation/EndOfRouteViewController.swift index 92ba0a0a9..ab30a5518 100644 --- a/MapboxNavigation/EndOfRouteViewController.swift +++ b/MapboxNavigation/EndOfRouteViewController.swift @@ -23,43 +23,60 @@ open class EndOfRouteTitleLabel: StylableLabel {} @objc(MBEndOfRouteStaticLabel) open class EndOfRouteStaticLabel: StylableLabel {} -/// :nodoc: -@objc(MBEndOfRouteCommentView) -open class EndOfRouteCommentView: StylableTextView {} - /// :nodoc: @objc(MBEndOfRouteButton) open class EndOfRouteButton: StylableButton {} @objc(MBEndOfRouteViewController) class EndOfRouteViewController: UIViewController { - // MARK: - IBOutlets - - @IBOutlet var labelContainer: UIView! - @IBOutlet var staticYouHaveArrived: EndOfRouteStaticLabel! - @IBOutlet var primary: UILabel! - @IBOutlet var endNavigationButton: UIButton! - @IBOutlet var stars: RatingControl! - @IBOutlet var commentView: UITextView! - @IBOutlet var commentViewContainer: UIView! - @IBOutlet var showCommentView: NSLayoutConstraint! - @IBOutlet var hideCommentView: NSLayoutConstraint! - @IBOutlet var ratingCommentsSpacing: NSLayoutConstraint! - // MARK: - Properties - lazy var placeholder: String = NSLocalizedString("END_OF_ROUTE_TITLE", bundle: .mapboxNavigation, value: "How can we improve?", comment: "Comment Placeholder Text") - lazy var endNavigation: String = NSLocalizedString("END_NAVIGATION", bundle: .mapboxNavigation, value: "End Navigation", comment: "End Navigation Button Text") - - typealias DismissHandler = (Int, String?) -> Void - var dismissHandler: DismissHandler? - var comment: String? - var rating: Int = 0 { - didSet { - self.rating == 0 ? self.hideComments() : self.showComments() - } - } - + lazy var column: UIStackView = { + let spacerView = UIView() + spacerView.setContentHuggingPriority(.defaultLow, for: .vertical) + spacerView.setContentCompressionResistancePriority(.defaultLow, for: .vertical) + let column = UIStackView(arrangedSubviews: [self.staticYouHaveArrived, self.primaryLabel, spacerView, self.endNavigationButton]) + column.axis = .vertical + column.alignment = .center + column.spacing = 8 + column.translatesAutoresizingMaskIntoConstraints = false + return column + }() + + lazy var staticYouHaveArrived: EndOfRouteStaticLabel = { + let label = EndOfRouteStaticLabel() + label.text = self.endOfRouteArrivedText + return label + }() + + lazy var primaryLabel: EndOfRouteTitleLabel = { + let label = EndOfRouteTitleLabel() + label.numberOfLines = 3 + label.adjustsFontSizeToFitWidth = true + return label + }() + + lazy var endNavigationButton: EndOfRouteButton = { + let button = EndOfRouteButton(type: .system) + button.setTitle(self.endNavigationText, for: .normal) + button.addTarget(self, action: #selector(self.endNavigationPressed(_:)), for: .touchUpInside) + return button + }() + + lazy var endOfRouteArrivedText: String = NSLocalizedString("END_OF_ROUTE_ARRIVED", bundle: .mapboxNavigation, value: "You have arrived", comment: "Title used for arrival") + lazy var endNavigationText: String = NSLocalizedString("END_NAVIGATION", bundle: .mapboxNavigation, value: "End Navigation", comment: "End Navigation Button Text") + + let dismissHandler: () -> Void + init(dismissHandler: @escaping () -> Void) { + self.dismissHandler = dismissHandler + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + open var destination: Waypoint? { didSet { guard isViewLoaded else { return } @@ -69,86 +86,32 @@ class EndOfRouteViewController: UIViewController { // MARK: - Lifecycle Methods - override func viewDidLoad() { - super.viewDidLoad() - self.clearInterface() - self.stars.didChangeRating = { [weak self] new in self?.rating = new } - self.setPlaceholderText() - self.styleCommentView() - self.commentViewContainer.alpha = 0.0 // setting initial hidden state + override func loadView() { + self.view = EndOfRouteContentView() + self.view.addSubview(self.column) + self.activateLayoutConstraints() } override func viewWillAppear(_ animated: Bool) { - super.viewWillDisappear(animated) - view.roundCorners([.topLeft, .topRight]) + super.viewWillAppear(animated) preferredContentSize.height = self.height(for: .normal) self.updateInterface() } - // MARK: - IBActions + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + // roundCorners needs to be called whenever the bounds change + view.roundCorners([.topLeft, .topRight]) + } - @IBAction func endNavigationPressed(_ sender: Any) { - self.dismissView() + // MARK: - Actions + + @objc func endNavigationPressed(_ sender: Any) { + self.dismissHandler() } // MARK: - Private Functions - private func styleCommentView() { - self.commentView.layer.cornerRadius = 6.0 - self.commentView.layer.borderColor = UIColor.lightGray.cgColor - self.commentView.layer.borderWidth = 1.0 - self.commentView.textContainerInset = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0) - } - - fileprivate func dismissView() { - let dismissal: () -> Void = { self.dismissHandler?(self.rating, self.comment) } - guard self.commentView.isFirstResponder else { return _ = dismissal() } - self.commentView.resignFirstResponder() - let fireTime = DispatchTime.now() + 0.3 // Not ideal, but works for now - DispatchQueue.main.asyncAfter(deadline: fireTime, execute: dismissal) - } - - private func showComments(animated: Bool = true) { - self.showCommentView.isActive = true - self.hideCommentView.isActive = false - self.ratingCommentsSpacing.constant = ConstraintSpacing.closer.rawValue - preferredContentSize.height = self.height(for: .commentShowing) - - let animate = { - self.view.layoutIfNeeded() - self.commentViewContainer.alpha = 1.0 - self.labelContainer.alpha = 0.0 - } - - let completion: (Bool) -> Void = { _ in self.labelContainer.isHidden = true } - let noAnimate = { animate(); completion(true) } - if animated { - UIView.animate(withDuration: 0.3, animations: animate, completion: nil) - } else { - noAnimate() - } - } - - private func hideComments(animated: Bool = true) { - self.labelContainer.isHidden = false - self.showCommentView.isActive = false - self.hideCommentView.isActive = true - self.ratingCommentsSpacing.constant = ConstraintSpacing.further.rawValue - preferredContentSize.height = self.height(for: .normal) - - let animate = { - self.view.layoutIfNeeded() - self.commentViewContainer.alpha = 0.0 - self.labelContainer.alpha = 1.0 - } - - let completion: (Bool) -> Void = { _ in self.commentViewContainer.isHidden = true } - let noAnimation = { animate(); completion(true) } - if animated { - UIView.animate(withDuration: 0.3, animations: animate, completion: nil) - } else { noAnimation() } - } - private func height(for height: ContainerHeight) -> CGFloat { let window = UIApplication.shared.keyWindow let bottomMargin = window!.safeAreaInsets.bottom @@ -156,50 +119,24 @@ class EndOfRouteViewController: UIViewController { } private func updateInterface() { - guard let name = destination?.name?.nonEmptyString else { return self.styleForUnnamedDestination() } - self.primary.text = name - } - - private func clearInterface() { - self.primary.text = nil - self.stars.rating = 0 - } - - private func styleForUnnamedDestination() { - self.staticYouHaveArrived.alpha = 0.0 - self.primary.text = NSLocalizedString("END_OF_ROUTE_ARRIVED", bundle: .mapboxNavigation, value: "You have arrived", comment: "Title used for arrival") - } - - private func setPlaceholderText() { - self.commentView.text = self.placeholder - } -} - -// MARK: - UITextViewDelegate - -extension EndOfRouteViewController: UITextViewDelegate { - func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { - guard text.count == 1, text.rangeOfCharacter(from: CharacterSet.newlines) != nil else { return true } - textView.resignFirstResponder() - return false - } - - func textViewDidChange(_ textView: UITextView) { - self.comment = textView.text // Bind data model - } - - func textViewDidBeginEditing(_ textView: UITextView) { - if textView.text == self.placeholder { - textView.text = nil - textView.alpha = 1.0 - } - textView.becomeFirstResponder() - } - - func textViewDidEndEditing(_ textView: UITextView) { - if (textView.text?.isEmpty ?? true) == true { - textView.text = self.placeholder - textView.alpha = 0.9 + guard let name = destination?.name?.nonEmptyString else { + self.staticYouHaveArrived.alpha = 0.0 + self.primaryLabel.text = self.endOfRouteArrivedText + return } + self.staticYouHaveArrived.alpha = 1.0 + self.primaryLabel.text = name + } + + private func activateLayoutConstraints() { + NSLayoutConstraint.activate([ + self.column.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16), + self.column.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -8), + self.column.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16), + self.column.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16), + self.endNavigationButton.heightAnchor.constraint(equalToConstant: 60), + self.endNavigationButton.leadingAnchor.constraint(equalTo: self.column.leadingAnchor), + self.endNavigationButton.trailingAnchor.constraint(equalTo: self.column.trailingAnchor) + ]) } } diff --git a/MapboxNavigation/RatingControl.swift b/MapboxNavigation/RatingControl.swift deleted file mode 100644 index 12560faf9..000000000 --- a/MapboxNavigation/RatingControl.swift +++ /dev/null @@ -1,136 +0,0 @@ -import CoreGraphics -import UIKit - -typealias RatingClosure = (Int) -> Void // rating - -/*@IBDesignable*/ -class RatingControl: UIStackView { - // MARK: Constants - - static let defaultSize = CGSize(width: 32.0, height: 32.0) - private let starTemplate = UIImage(named: "star", in: .mapboxNavigation, compatibleWith: nil) - - // MARK: Properties - - private var stars = [UIButton]() - - var didChangeRating: RatingClosure? - - var rating: Int = 0 { - didSet { - self.updateSelectionStates() - self.didChangeRating?(self.rating) - } - } - - @objc public dynamic var selectedColor: UIColor = #colorLiteral(red: 0.1205472574, green: 0.2422055006, blue: 0.3489340544, alpha: 1) { - didSet { - updateSelectionStates() - } - } - - @objc public dynamic var normalColor: UIColor = #colorLiteral(red: 0.8508961797, green: 0.8510394692, blue: 0.850877285, alpha: 1) { - didSet { - updateSelectionStates() - } - } - - @objc public dynamic var starSize: CGSize = defaultSize { - didSet { - self.configureStars() - } - } - - @objc public dynamic var starCount: Int = 5 { - didSet { - self.configureStars() - } - } - - // MARK: Initializers - - override public init(frame: CGRect) { - super.init(frame: frame) - self.configureStars() - } - - public required init(coder: NSCoder) { - super.init(coder: coder) - self.configureStars() - } - - // MARK: Private Functions - - private func configureStars() { - self.removeStars() - self.addStars() - self.updateSelectionStates() - } - - private func addStars() { - for index in 0 ..< self.starCount { - let button = UIButton(type: .custom) - button.setImage(self.starTemplate, for: .normal) - button.adjustsImageWhenHighlighted = false - self.addButtonSizeConstraints(to: button) - - let setRatingNumber = index + 1 - let localizedString = NSLocalizedString("RATING_ACCESSIBILITY_SET", bundle: .mapboxNavigation, value: "Set %ld-star rating", comment: "Format for accessibility label of button for setting a rating; 1 = number of stars") - button.accessibilityLabel = String.localizedStringWithFormat(localizedString, setRatingNumber) - - button.addTarget(self, action: #selector(RatingControl.ratingButtonTapped(button:)), for: .touchUpInside) - - addArrangedSubview(button) - - self.stars.append(button) - } - } - - private func removeStars() { - for star in self.stars { - removeArrangedSubview(star) - star.removeFromSuperview() - } - self.stars.removeAll() - } - - private func updateSelectionStates() { - for (index, button) in self.stars.enumerated() { - let selected = index < self.rating - button.tintColor = selected ? self.selectedColor : self.normalColor - button.isSelected = selected - - self.setAccessibility(for: button, at: index) - } - } - - private func setAccessibility(for button: UIButton, at index: Int) { - self.setAccessibilityHint(for: button, at: index) - - let value: String = if self.rating == 0 { - NSLocalizedString("NO_RATING", bundle: .mapboxNavigation, value: "No rating set.", comment: "Accessibility value of label indicating the absence of a rating") - } else { - String.localizedStringWithFormat(NSLocalizedString("RATING_STARS_FORMAT", bundle: .mapboxNavigation, value: "%ld star(s) set.", comment: "Format for accessibility value of label indicating the existing rating; 1 = number of stars"), self.rating) - } - - button.accessibilityValue = value - } - - private func setAccessibilityHint(for button: UIButton, at index: Int) { - guard self.rating == (index + 1) else { return } // This applies only to the zero-resettable button. - - button.accessibilityHint = NSLocalizedString("RATING_ACCESSIBILITY_RESET", bundle: .mapboxNavigation, value: "Tap to reset the rating to zero.", comment: "Rating Reset To Zero Accessability Hint") - } - - private func addButtonSizeConstraints(to view: UIView) { - view.widthAnchor.constraint(equalToConstant: self.starSize.width).isActive = true - view.heightAnchor.constraint(equalToConstant: self.starSize.height).isActive = true - } - - @objc private func ratingButtonTapped(button sender: UIButton) { - guard let index = stars.firstIndex(of: sender) else { return assertionFailure("RatingControl.swift: The Star button that was tapped was not found in the RatingControl.stars array. This should never happen.") } - let selectedRating = index + 1 - - self.rating = (selectedRating == self.rating) ? 0 : selectedRating - } -} diff --git a/MapboxNavigation/Resources/Base.lproj/Navigation.storyboard b/MapboxNavigation/Resources/Base.lproj/Navigation.storyboard deleted file mode 100644 index 893359a1f..000000000 --- a/MapboxNavigation/Resources/Base.lproj/Navigation.storyboard +++ /dev/null @@ -1,200 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/MapboxNavigation/Resources/Base.lproj/Navigation.strings b/MapboxNavigation/Resources/Base.lproj/Navigation.strings deleted file mode 100644 index 73acb1fb1..000000000 --- a/MapboxNavigation/Resources/Base.lproj/Navigation.strings +++ /dev/null @@ -1,3 +0,0 @@ - -/* Class = "UILabel"; text = "Rate your trip"; ObjectID = "W5U-cV-cDO"; */ -"W5U-cV-cDO.text" = "Rate your trip"; diff --git a/MapboxNavigation/RouteMapViewController.swift b/MapboxNavigation/RouteMapViewController.swift index 1157b6dc7..37d8efed3 100644 --- a/MapboxNavigation/RouteMapViewController.swift +++ b/MapboxNavigation/RouteMapViewController.swift @@ -124,11 +124,17 @@ class RouteMapViewController: UIViewController { right: 20) } - lazy var endOfRouteViewController: EndOfRouteViewController = { - let storyboard = UIStoryboard(name: "Navigation", bundle: .mapboxNavigation) - let viewController = storyboard.instantiateViewController(withIdentifier: "EndOfRouteViewController") as! EndOfRouteViewController - return viewController - }() + lazy var endOfRouteViewController: EndOfRouteViewController = .init(dismissHandler: { [weak self] in + guard let self else { + return + } + guard let routeController else { + assertionFailure("routeController was unexpectedly nil") + return + } + routeController.endNavigation() + self.delegate?.mapViewControllerDidFinish(self, byCanceling: false) + }) weak var delegate: RouteMapViewControllerDelegate? diff --git a/MapboxNavigation/UIView.swift b/MapboxNavigation/UIView.swift index 91512381b..6ad588d66 100644 --- a/MapboxNavigation/UIView.swift +++ b/MapboxNavigation/UIView.swift @@ -70,7 +70,10 @@ extension UIView { } func pinInSuperview(respectingMargins margins: Bool = false) { - guard let superview else { return } + guard let superview else { + assertionFailure("superview was unexpectedly nil") + return + } let guide: Anchorable = margins ? superview.layoutMarginsGuide : superview let constraints = [