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

Adding generic shield support to instruction banner. #1417

Merged
merged 24 commits into from
May 18, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
fd2a57b
Adding generic shield support to instruction banner.
May 11, 2018
58646ea
Improving readability by rewriting delimiter case
May 11, 2018
7229115
Updating lockfile in effort to chase down elusive bug
May 11, 2018
e21b226
Fixing issue where public initializer's signature has changed, causin…
May 11, 2018
b2b96ca
updating references in test to new enum
May 11, 2018
261d2a0
Reverting dependancy on MapboxDirections back to 0.20.0
May 15, 2018
76899bf
Merge branch 'master' into jerrad/generic-route-shields
May 15, 2018
5a1f541
taking `GenericRouteShield` public
May 16, 2018
873a3a2
Fixing incorrect logic where we only check for one of the two book-ca…
May 16, 2018
6b19ea3
Fixing test failures caused by improper fixtures.
May 16, 2018
30dabb6
Updating test to pre-cache the shield image before it requests it
May 16, 2018
3c1795a
Updating test logic to reflect updated, correct delimiter behavior. E…
May 16, 2018
e70593e
Red Light: Adding test to make sure that Generic Shields load as expe…
May 18, 2018
218f133
Green Light: Fixing issue by ensuring that generic shield components …
May 18, 2018
cdf29cd
Does this really need to be a function? also removing unneeded guard …
May 18, 2018
273a739
Adding test to make sure generic instructions are handled properly
May 18, 2018
7606f48
Adding snapshot test for generic shields.
May 18, 2018
d566413
updating CHANGELOG.md
May 18, 2018
45aa460
Adding option in FBSnapshot's verify command to only check "Reference…
May 18, 2018
304d31c
Fixing bad test caused by wonky reference image. Also adding the abil…
May 18, 2018
e578eb0
While we're at it, lets update the other snapshot tests with this arg…
May 18, 2018
bdecb49
Adding ability for generic shield components to cache off the current…
May 18, 2018
a6ebc9e
Removing default case for cacheKey switch logic
May 18, 2018
b7eb8c8
Adding comments
May 18, 2018
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
2 changes: 1 addition & 1 deletion Cartfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
binary "https://www.mapbox.com/ios-sdk/Mapbox-iOS-SDK.json" ~> 4.0
github "mapbox/MapboxDirections.swift" ~> 0.20.0
github "mapbox/MapboxDirections.swift" "jerrad/generic-icon" #~> 0.20.0
github "mapbox/turf-swift" ~> 0.1
github "mapbox/mapbox-events-ios" ~> 0.4
github "ceeK/Solar" ~> 2.1.0
Expand Down
4 changes: 4 additions & 0 deletions MapboxNavigation.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@
8D54F14A206ECF720038736D /* InstructionPresenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D54F149206ECF720038736D /* InstructionPresenterTests.swift */; };
8D5DFFF1207C04840093765A /* NSAttributedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D5DFFF0207C04840093765A /* NSAttributedString.swift */; };
8D8EA9BC20575CD80077F478 /* FeedbackCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D8EA9BB20575CD80077F478 /* FeedbackCollectionViewCell.swift */; };
8D9ADEA720A0C61A0067E845 /* GenericRouteShield.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D9ADEA620A0C61A0067E845 /* GenericRouteShield.swift */; };
8D9CD7FF20880581004DC4B3 /* XCTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D9CD7FD20880581004DC4B3 /* XCTestCase.swift */; };
8DB45E90201698EB001EA6A3 /* UIStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DB45E8F201698EB001EA6A3 /* UIStackView.swift */; };
8DB63A3A1FBBCA2200928389 /* RatingControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DB63A391FBBCA2200928389 /* RatingControl.swift */; };
Expand Down Expand Up @@ -584,6 +585,7 @@
8D54F149206ECF720038736D /* InstructionPresenterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstructionPresenterTests.swift; sourceTree = "<group>"; };
8D5DFFF0207C04840093765A /* NSAttributedString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSAttributedString.swift; sourceTree = "<group>"; };
8D8EA9BB20575CD80077F478 /* FeedbackCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackCollectionViewCell.swift; sourceTree = "<group>"; };
8D9ADEA620A0C61A0067E845 /* GenericRouteShield.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericRouteShield.swift; sourceTree = "<group>"; };
8D9CD7FD20880581004DC4B3 /* XCTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCTestCase.swift; sourceTree = "<group>"; };
8DB45E8F201698EB001EA6A3 /* UIStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIStackView.swift; sourceTree = "<group>"; };
8DB63A391FBBCA2200928389 /* RatingControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RatingControl.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1124,6 +1126,7 @@
children = (
351BEC091E5BCC72006FE110 /* DashedLineView.swift */,
8D53136A20653FA20044891E /* ExitView.swift */,
8D9ADEA620A0C61A0067E845 /* GenericRouteShield.swift */,
351BEC031E5BCC6C006FE110 /* LaneView.swift */,
359A8AEE1FA7B25800BDB486 /* LanesStyleKit.swift */,
351BEBED1E5BCC63006FE110 /* ManeuversStyleKit.swift */,
Expand Down Expand Up @@ -1833,6 +1836,7 @@
8D391CE21FD71E78006BB91F /* Waypoint.swift in Sources */,
353EC9D71FB09708002EB0AB /* StepsViewController.swift in Sources */,
35726EE81F0856E900AFA1B6 /* DayStyle.swift in Sources */,
8D9ADEA720A0C61A0067E845 /* GenericRouteShield.swift in Sources */,
35DC9D911F4323AA001ECD64 /* LanesView.swift in Sources */,
359D1B281FFE70D30052FA42 /* NavigationView.swift in Sources */,
8DE879661FBB9980002F06C0 /* EndOfRouteViewController.swift in Sources */,
Expand Down
5 changes: 5 additions & 0 deletions MapboxNavigation/DayStyle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ open class DayStyle: Style {
ExitView.appearance().foregroundColor = .black
FloatingButton.appearance().backgroundColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
FloatingButton.appearance().tintColor = tintColor
GenericRouteShield.appearance().backgroundColor = .clear
GenericRouteShield.appearance().borderWidth = 1.0
GenericRouteShield.appearance().cornerRadius = 5.0
GenericRouteShield.appearance().foregroundColor = .black
Copy link
Contributor

Choose a reason for hiding this comment

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

How does this look for the night style?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Pretty good, all we end up having to do is needing to change the foregroundColor: Line

Example

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh i missed that, 👍

InstructionsBannerContentView.appearance().backgroundColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
InstructionsBannerView.appearance().backgroundColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
LaneView.appearance().primaryColor = .defaultLaneArrowPrimary
Expand Down Expand Up @@ -193,6 +197,7 @@ open class NightStyle: DayStyle {
ExitView.appearance().foregroundColor = .white
FloatingButton.appearance().backgroundColor = backgroundColor
FloatingButton.appearance().tintColor = #colorLiteral(red: 0.9842069745, green: 0.9843751788, blue: 0.9841964841, alpha: 1)
GenericRouteShield.appearance().foregroundColor = .white
InstructionsBannerContentView.appearance().backgroundColor = backgroundColor
InstructionsBannerView.appearance().backgroundColor = backgroundColor
LaneView.appearance().primaryColor = #colorLiteral(red: 0.9842069745, green: 0.9843751788, blue: 0.9841964841, alpha: 1)
Expand Down
85 changes: 85 additions & 0 deletions MapboxNavigation/GenericRouteShield.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import Foundation
import UIKit

class GenericRouteShield: StylableView {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this class be public, so that developers can customize the colors or roundedness?

static let labelFontSizeScaleFactor: CGFloat = 2.0/3.0

@objc dynamic var foregroundColor: UIColor? {
didSet {
layer.borderColor = foregroundColor?.cgColor
routeLabel.textColor = foregroundColor
setNeedsDisplay()
}
}

lazy var routeLabel: UILabel = {
let label: UILabel = .forAutoLayout()
label.text = routeText
label.textColor = .black
label.font = UIFont.boldSystemFont(ofSize: pointSize * ExitView.labelFontSizeScaleFactor)

return label
}()

var routeText: String? {
didSet {
routeLabel.text = routeText
invalidateIntrinsicContentSize()
}
}
var pointSize: CGFloat {
didSet {
routeLabel.font = routeLabel.font.withSize(pointSize * ExitView.labelFontSizeScaleFactor)
rebuildConstraints()
}
}

convenience init(pointSize: CGFloat, text: String) {
self.init(frame: .zero)
self.pointSize = pointSize
self.routeText = text
commonInit()
}

override init(frame: CGRect) {
pointSize = 0.0
super.init(frame: frame)
}

required init?(coder aDecoder: NSCoder) {
pointSize = 0.0
super.init(coder: aDecoder)
commonInit()
}

func rebuildConstraints() {
NSLayoutConstraint.deactivate(self.constraints)
buildConstraints()
}

func commonInit() {
translatesAutoresizingMaskIntoConstraints = false
layer.masksToBounds = true

//build view hierarchy
addSubview(routeLabel)
buildConstraints()

setNeedsLayout()
invalidateIntrinsicContentSize()
layoutIfNeeded()
}

func buildConstraints() {
let height = heightAnchor.constraint(equalToConstant: pointSize * 1.2)

let labelCenterY = routeLabel.centerYAnchor.constraint(equalTo: centerYAnchor)

let labelLeading = routeLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8)
let labelTrailingSpacing = routeLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8)

let constraints = [height, labelCenterY, labelLeading, labelTrailingSpacing]

addConstraints(constraints)
}
}
89 changes: 60 additions & 29 deletions MapboxNavigation/InstructionPresenter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,37 +83,44 @@ class InstructionPresenter {
strings.append(attributedStrings.reduce(initial, +))
}

switch component.type {
//Throw away exit components. We know this is safe because we know that if there is an exit component,
// there is an exit code component, and the latter contains the information we care about.
guard component.type != .exit else { continue }

//If we have a exit, in the first two components, lets handle that first.
if component.type == .exitCode, 0...1 ~= index,
let exitString = attributedString(forExitComponent: component, maneuverDirection: instruction.maneuverDirection, dataSource: dataSource) {
case .exit:
continue

//If we have a exit, in the first two components, lets handle that.
case .exitCode where 0...1 ~= index:
guard let exitString = self.attributedString(forExitComponent: component, maneuverDirection: instruction.maneuverDirection, dataSource: dataSource) else { fallthrough }
build(component, [exitString])
}

//If we have a shield, lets include those
else if let shieldString = attributedString(forShieldComponent: component, repository: imageRepository, dataSource: dataSource, onImageDownload: onImageDownload) {
build(component, [joinString, shieldString])
}

else {
//if it's a delimiter, skip it if it's between two shields. Otherwise, process the regular text component.
if component.type == .delimiter {

let componentBefore = components.component(before: component)
let componentAfter = components.component(after: component)

if let shieldKey = componentBefore?.cacheKey(),
imageRepository.cachedImageForKey(shieldKey) != nil {
continue
}
if let shieldKey = componentAfter?.cacheKey(),
imageRepository.cachedImageForKey(shieldKey) != nil {
continue
}
//If we have an icon component, lets turn it into a shield.
case .icon:
if let shieldString = attributedString(forShieldComponent: component, repository: imageRepository, dataSource: dataSource, onImageDownload: onImageDownload) {
build(component, [joinString, shieldString])
} else if let genericShieldString = attributedString(forGenericShield: component, dataSource: dataSource) {
build(component, [joinString, genericShieldString])
} else {
fallthrough
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a clever use of a switch statement, but do you think the fallthroughs inside conditionals make the code flow a little more difficult to keep track of?

Copy link
Contributor Author

@JThramer JThramer May 11, 2018

Choose a reason for hiding this comment

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

@1ec5 It's certainly a complex switch, but I'd make the case that it's a heck of a lot easier to follow than the mess of if statements before it.

I like to think that the fallthrough statements enforce the idea that everything falls-back to text.

Copy link
Contributor

Choose a reason for hiding this comment

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

Would it make sense to factor this switch out into it's own function? This is function is getting boarder-line large.

}

//if it's a delimiter, skip it if it's between two shields.
case .delimiter:
let componentBefore = components.component(before: component)
let componentAfter = components.component(after: component)

if let shieldKey = componentBefore?.cacheKey(),
imageRepository.cachedImageForKey(shieldKey) != nil {
continue
}
if let shieldKey = componentAfter?.cacheKey(),
imageRepository.cachedImageForKey(shieldKey) != nil {
continue
}
fallthrough

//Otherwise, process as text component.
default:
guard let componentString = attributedString(forTextComponent: component, dataSource: dataSource) else { continue }
build(component, [joinString, componentString])
}
Expand All @@ -130,8 +137,14 @@ class InstructionPresenter {
return exitString
}

func attributedString(forGenericShield component: VisualInstructionComponent, dataSource: DataSource) -> NSAttributedString? {
guard component.type == .icon, let text = component.text, let key = component.cacheKey() else { return nil }

return genericShield(text: text, cacheKey: key, dataSource: dataSource)
}

func attributedString(forShieldComponent shield: VisualInstructionComponent, repository:ImageRepository, dataSource: DataSource, onImageDownload: @escaping ImageDownloadCompletion) -> NSAttributedString? {
guard let shieldKey = shield.cacheKey() else { return nil }
guard shield.imageURL != nil, let shieldKey = shield.cacheKey() else { return nil }

//If we have the shield already cached, use that.
if let cachedImage = repository.cachedImageForKey(shieldKey) {
Expand All @@ -141,8 +154,8 @@ class InstructionPresenter {
// Let's download the shield
shieldImageForComponent(shield, in: repository, height: dataSource.shieldHeight, completion: onImageDownload)

//and return the shield's code for usage in the meantime until download is complete.
return attributedString(forTextComponent: shield, dataSource: dataSource)
//Return nothing in the meantime, triggering downstream behavior (generic shield or text)
return nil
}

func attributedString(forTextComponent component: VisualInstructionComponent, dataSource: DataSource) -> NSAttributedString? {
Expand Down Expand Up @@ -182,6 +195,24 @@ class InstructionPresenter {
return NSAttributedString(attachment: attachment)
}

private func genericShield(text: String, cacheKey: String, dataSource: DataSource) -> NSAttributedString? {
let view = GenericRouteShield(pointSize: dataSource.font.pointSize, text: text)

let attachment = ExitAttachment()

if let image = imageRepository.cachedImageForKey(cacheKey) {
attachment.image = image
} else {
guard let image = takeSnapshot(on: view) else { return nil }
imageRepository.storeImage(image, forKey: cacheKey, toDisk: false)
attachment.image = image
}

attachment.font = dataSource.font

return NSAttributedString(attachment: attachment)
}

private func exitShield(side: ExitSide = .right, text: String, component: VisualInstructionComponent, dataSource: DataSource) -> NSAttributedString? {

let view = ExitView(pointSize: dataSource.font.pointSize, side: side, text: text)
Expand Down
2 changes: 1 addition & 1 deletion MapboxNavigation/ManeuverView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ open class ManeuverView: UIView {
ManeuversStyleKit.drawFork(frame: bounds, resizing: resizing, primaryColor: primaryColor, secondaryColor: secondaryColor)
flip = [.left, .slightLeft, .sharpLeft].contains(direction)
case .takeRoundabout, .turnAtRoundabout, .takeRotary:
ManeuversStyleKit.drawRoundabout(frame: bounds, resizing: resizing, primaryColor: primaryColor, secondaryColor: secondaryColor, roundabout_angle: CGFloat(visualInstruction.primaryInstruction.degrees))
ManeuversStyleKit.drawRoundabout(frame: bounds, resizing: resizing, primaryColor: primaryColor, secondaryColor: secondaryColor, roundabout_angle: CGFloat(visualInstruction.primaryInstruction.finalHeading))
flip = visualInstruction.drivingSide == .left

case .arrive:
Expand Down
6 changes: 3 additions & 3 deletions MapboxNavigation/VisualInstructionComponent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ extension VisualInstructionComponent {
case .exit, .exitCode:
guard let exitCode = self.text else { return nil}
return "exit-" + exitCode + "-\(VisualInstructionComponent.scale)-\(hashValue)"
case .image, .text:
guard let imageURL = imageURL else { return nil }
case .icon:
guard let imageURL = imageURL else { return "generic-" + (text ?? "nil") }
return "\(imageURL.absoluteString)-\(VisualInstructionComponent.scale)"
case .delimiter:
default:
Copy link
Contributor

Choose a reason for hiding this comment

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

It might make more sense to keep .delimiter here and remove default so as new cases are added, the compiler will yell at us to handle them.

return nil
}
}
Expand Down