ส่วนนี้จะอธิบายวิธีใช้ Navigation SDK กับไลบรารี Apple CarPlay เพื่อแสดงประสบการณ์การนำทางของแอปบนอุปกรณ์สวมศีรษะในรถยนต์ หากระบบในรถยนต์ของคนขับรองรับ CarPlay ผู้ขับขี่สามารถใช้แอปของคุณได้โดยตรงบนจอแสดงผลของรถโดยเชื่อมต่อโทรศัพท์กับเครื่อง การนำทางด้วยเสียงจะทำงาน ในลำโพงรถด้วย
คุณสร้างแอป CarPlay จากชุดเทมเพลต UI จาก Apple แอปของคุณมีหน้าที่เลือกเทมเพลตที่จะแสดงและระบุข้อมูลภายใน
ระบบในแดชบอร์ดแสดงองค์ประกอบแบบอินเทอร์แอกทีฟที่ได้รับการอนุมัติด้านความปลอดภัย เพื่อให้คนขับนำทางไปยังจุดหมายได้อย่างปลอดภัยโดยไม่เกิดสิ่งรบกวนที่ไม่เหมาะสม คุณยังสามารถตั้งโปรแกรมแอปของคุณให้คนขับโต้ตอบกับฟีเจอร์เฉพาะของแอปได้ เช่น การยอมรับหรือปฏิเสธคำสั่งซื้อ หรือการดูตำแหน่งของลูกค้าบนแผนที่ นอกจากนี้ ยังสามารถตั้งโปรแกรมอัปเดตสถานะการสั่งซื้อให้ปรากฏบนหน่วยแดชบอร์ดได้ด้วย
![การแสดงการนำทางของ CarPlay และโทรศัพท์](https://developers.google.cn/static/maps/documentation/navigation/ios-sdk/images/carplay-and-phone.png?hl=th)
ตั้งค่า
เริ่มด้วย CarPlay
ขั้นแรก ให้ทำความคุ้นเคยกับเอกสารประกอบของ Apple:
- https://developer.apple.com/carplay/
- https://developer.apple.com/carplay/documentation/CarPlay-App-Programming-Guide.pdf
ตั้งค่า Navigation SDK
- เมื่อคุณได้อ่านเอกสารประกอบของ Apple แล้ว คุณก็พร้อมที่จะใช้งาน Navigation SDK ได้
- ตั้งค่าโปรเจ็กต์หากคุณยังไม่ได้ผสานรวม Navigation SDK เข้ากับแอป
- เปิดใช้ฟีดคำแนะนำ TurnByTurn สำหรับแอปของคุณ
- ไม่บังคับ ใช้ไอคอนที่สร้างขึ้นจาก Navigation SDK
- วาดแผนที่โดยใช้คลาส
GMSMapView
ที่ให้ไว้ในคลาส UIView ดูข้อมูลเพิ่มเติมที่ไปยังส่วนต่างๆ ของเส้นทาง ป้อนข้อมูลCPNavigationSession
ด้วยข้อมูลจากไลบรารี TurnByTurn
วาด UI แผนที่และการนำทาง
คลาส GMSMapView
จะแสดงผลแผนที่และ CPMapTemplate
แสดงผล UI ในหน้าจอ CarPlay โดยมีฟังก์ชันการทำงานเหมือนกับ GMSMapView
สำหรับโทรศัพท์เป็นส่วนใหญ่ แต่มีการโต้ตอบที่จำกัด
Swift
init(window: CPWindow) {
super.init(nibName: nil, bundle: nil)
self.window = window
// More CPMapTemplate initialization
}
override func viewDidLoad() {
super.viewDidLoad()
let mapViewOptions = GMSMapViewOptions()
mapViewOptions.screen = window.screen
mapViewOptions.frame = self.view.bounds
mapView = GMSMapView(options: mapViewOptions)
mapView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
mapView.settings.isNavigationHeaderEnabled = false
mapView.settings.isNavigationFooterEnabled = false
// Disable buttons: in CarPlay, no part of the map is clickable.
// The app should instead place these buttons in the appropriate slots of the CarPlay template.
mapView.settings.compassButton = false
mapView.settings.isRecenterButtonEnabled = false
mapView.shouldDisplaySpeedometer = false
mapView.isMyLocationEnabled = true
self.view.addSubview(mapView)
}
Objective-C
- (instancetype)initWithWindow:(CPWindow *)window {
self = [super initWithNibName:nil bundle:nil];
if (self) {
_window = window;
// More CPMapTemplate initialization
}
}
- (void)viewDidLoad {
[super viewDidLoad];
GMSMapViewOptions *options = [[GMSMapViewOptions alloc] init];
options.screen = _window.screen;
options.frame = self.view.bounds;
_mapView = [[GMSMapView alloc] initWithOptions:options];
_mapView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
_mapView.settings.navigationHeaderEnabled = NO;
_mapView.settings.navigationFooterEnabled = NO;
// Disable buttons: in CarPlay, no part of the map is clickable.
// The app should instead place these buttons in the appropriate slots of the CarPlay template.
_mapView.settings.compassButton = NO;
_mapView.settings.recenterButtonEnabled = NO;
_mapView.shouldDisplaySpeedometer = NO;
_mapView.myLocationEnabled = YES;
[self.view addSubview:_mapView];
}
เปิดใช้การโต้ตอบบนแผนที่
CarPlay จำกัดการโต้ตอบกับพื้นผิวหน้าจอไว้ตามวิธีของ CPMapTemplateDelegate
หลายๆ วิธีเพื่อความปลอดภัยของผู้ขับขี่ ใช้โค้ดเรียกกลับเหล่านี้เพื่อรองรับการโต้ตอบของผู้ขับขี่แบบจำกัดกับแผนที่บนหน้าจอในแดชบอร์ด
หากต้องการรองรับการดำเนินการเพิ่มเติมของผู้ใช้ ให้สร้างอาร์เรย์ของ CPMapButton
แล้วมอบหมายให้กับ CPMapTemplate.mapButtons
โค้ดต่อไปนี้จะสร้างการโต้ตอบแบบเลื่อนและปุ่มสำหรับเลื่อน ซูมเข้าและออก และเพื่อแสดงตำแหน่งของผู้ใช้
การโต้ตอบเมื่อเลื่อน
Swift
// MARK: CPMapTemplateDelegate
func mapTemplate(_ mapTemplate: CPMapTemplate, panBeganWith direction: CPMapTemplate.PanDirection) {
}
func mapTemplate(_ mapTemplate: CPMapTemplate, panWith direction: CPMapTemplate.PanDirection) {
let scrollAmount = scrollAmount(for: direction)
let scroll = GMSCameraUpdate.scrollBy(x: scrollAmount.x, y: scrollAmount.y)
mapView.animate(with: scroll)
}
func mapTemplate(_ mapTemplate: CPMapTemplate, panEndedWith direction: CPMapTemplate.PanDirection) {
}
func scrollAmount(for panDirection: CPMapTemplate.PanDirection) -> CGPoint {
let scrollDistance = 80.0
var scrollAmount = CGPoint(x: 0, y: 0)
switch panDirection {
case .left:
scrollAmount.x -= scrollDistance
break;
case .right:
scrollAmount.x += scrollDistance
break;
case .up:
scrollAmount.y += scrollDistance
break;
case .down:
scrollAmount.y -= scrollDistance
break;
default:
break;
}
if scrollAmount.x != 0 && scrollAmount.y != 0 {
// Adjust length if scrolling diagonally.
scrollAmount = CGPointMake(scrollAmount.x * sqrt(1.0/2.0), scrollAmount.y * sqrt(1.0/2.0))
}
return scrollAmount
}
Objective-C
#pragma mark - CPMapTemplateDelegate
- (void)mapTemplate:(CPMapTemplate *)mapTemplate panBeganWithDirection:(CPPanDirection)direction {
}
- (void)mapTemplate:(CPMapTemplate *)mapTemplate panWithDirection:(CPPanDirection)direction {
CGPoint scrollAmount = [self scrollAmountForPanDirection:direction];
GMSCameraUpdate *scroll = [GMSCameraUpdate scrollByX:scrollAmount.x Y:scrollAmount.y];
[_mapView animateWithCameraUpdate:scroll];
}
- (void)mapTemplate:(CPMapTemplate *)mapTemplate panEndedWithDirection:(CPPanDirection)direction {
}
- (CGPoint)scrollAmountForPanDirection:(CPPanDirection)direction {
static const CGFloat scrollDistance = 80.;
CGPoint scrollAmount = {0., 0.};
if (direction & CPPanDirectionLeft) {
scrollAmount.x = -scrollDistance;
}
if (direction & CPPanDirectionRight) {
scrollAmount.x = scrollDistance;
}
if (direction & CPPanDirectionUp) {
scrollAmount.y = -scrollDistance;
}
if (direction & CPPanDirectionDown) {
scrollAmount.y = scrollDistance;
}
if (scrollAmount.x != 0 && scrollAmount.y != 0) {
// Adjust length if scrolling diagonally.
scrollAmount =
CGPointMake(scrollAmount.x * (CGFloat)M_SQRT1_2, scrollAmount.y * (CGFloat)M_SQRT1_2);
}
return scrollAmount;
}
การใช้งานปุ่มทั่วไป
Swift
// MARK: Create Buttons
func createMapButtons() -> [CPMapButton] {
let panButton = mapButton(systemImageName: "dpad.fill") { [weak self] in
self?.didTapPanButton()
}
let zoomOutButton = mapButton(systemImageName: "minus.magnifyingglass") { [weak self] in
self?.didTapZoomOutButton()
}
let zoomInButton = mapButton(systemImageName: "plus.magnifyingglass") { [weak self] in
self?.didTapZoomInButton()
}
let myLocationButton = mapButton(systemImageName: "location") { [weak self] in
self?.didTapMyLocationButton()
}
let mapButtons = [panButton, zoomOutButton, zoomInButton, myLocationButton]
return mapButtons
}
func mapButton(systemImageName: String, handler: @escaping () -> Void) -> CPMapButton {
}
// MARK: Button callbacks
@objc func didTapPanButton() {
mapTemplate?.showPanningInterface(animated: true)
}
@objc func didTapZoomOutButton() {
mapView.animate(with: GMSCameraUpdate.zoomOut())
}
@objc func didTapZoomInButton() {
mapView.animate(with: GMSCameraUpdate.zoomIn())
}
@objc func didTapMyLocationButton() {
if let lastLocation = lastLocation {
let cameraPosition = GMSCameraPosition(target: lastLocation.coordinate, zoom: 15)
mapView.animate(to: cameraPosition)
}
}
Objective-C
#pragma mark - Create Buttons
- (NSArray<CPMapButton *>*)createMapButtons {
NSMutableArray<CPMapButton *> *mapButtons = [NSMutableArray<CPMapButton *> array];
__weak __typeof__(self) weakSelf = self;
CPMapButton *panButton = [self mapButtonWithSystemImageNamed:@"dpad.fill"
handler:^(CPMapButton *_) {
[weakSelf didTapPanButton];
}];
[mapButtons addObject:panButton];
CPMapButton *zoomOutButton =
[self mapButtonWithSystemImageNamed:@"minus.magnifyingglass"
handler:^(CPMapButton *_Nonnull mapButon) {
[weakSelf didTapZoomOutButton];
}];
[mapButtons addObject:zoomOutButton];
CPMapButton *zoomInButton =
[self mapButtonWithSystemImageNamed:@"plus.magnifyingglass"
handler:^(CPMapButton *_Nonnull mapButon) {
[weakSelf didTapZoomInButton];
}];
[mapButtons addObject:zoomInButton];
CPMapButton *myLocationButton =
[self mapButtonWithSystemImageNamed:@"location"
handler:^(CPMapButton *_Nonnull mapButton) {
[weakSelf didTapMyLocationButton];
}];
[mapButtons addObject:myLocationButton];
return mapButtons;
}
#pragma mark - Button Callbacks
- (void)didTapZoomOutButton {
[_mapView animateWithCameraUpdate:[GMSCameraUpdate zoomOut]];
}
- (void)didTapZoomInButton {
[_mapView animateWithCameraUpdate:[GMSCameraUpdate zoomIn]];
}
- (void)didTapMyLocationButton {
CLLocation *location = self.lastLocation;
if (location) {
GMSCameraPosition *position =
[[GMSCameraPosition alloc] initWithTarget:self.lastLocation.coordinate zoom:15.];
[_mapView animateToCameraPosition:position];
}
}
- (void)didTapPanButton {
[_mapTemplate showPanningInterfaceAnimated:YES];
_isPanningInterfaceEnabled = YES;
}
- (void)didTapStopPanningButton {
[_mapTemplate dismissPanningInterfaceAnimated:YES];
_isPanningInterfaceEnabled = NO;
}
หมายเหตุ: ไม่สามารถเลือกเส้นทางอื่นบนหน้าจอ CarPlay โดยจะต้องเลือกจากโทรศัพท์ก่อนที่ CarPlay จะเริ่มต้น
แสดงเส้นทางการนำทาง
ส่วนนี้จะครอบคลุมวิธีตั้งค่า Listener สำหรับฟีดข้อมูลและวิธีป้อนข้อมูลเส้นทางการนำทางในแผงคำแนะนำและค่าประมาณการเดินทาง ดูข้อมูลเพิ่มเติมได้ที่ส่วน "สร้างแอปการนำทางของ CarPlay" ของ คู่มือการเขียนโปรแกรมแอป CarPlay
แผงคำแนะนำและค่าประมาณการเดินทางมีการ์ดการนำทางที่แสดงข้อมูลการนำทางที่เกี่ยวข้องกับการเดินทางปัจจุบัน ไลบรารีของ TurnByTurn ใน SDK การนำทางสามารถช่วยให้ข้อมูลนี้ เช่น สัญลักษณ์ ข้อความ และเวลาที่เหลืออยู่
ตั้งค่า Listener
ทำตามวิธีการตั้งค่า Listener เหตุการณ์ในเปิดใช้ฟีดข้อมูลแบบเลี้ยวต่อเลี้ยว
เติมข้อมูลการนำทาง
ส่วนแรกของตัวอย่างโค้ดต่อไปนี้จะแสดงวิธีสร้างค่าประมาณการเดินทางของ CarPlay โดยแปล GMSNavigationNavInfo.timeToCurrentStepSeconds
เป็น CPTravelEstimate
อ่านข้อมูลเพิ่มเติมเกี่ยวกับองค์ประกอบเหล่านี้และองค์ประกอบการแสดงผลอื่นๆ ได้ในเปิดใช้ฟีดข้อมูลแบบเลี้ยวต่อเลี้ยว
ส่วนที่ 2 ของตัวอย่างจะแสดงวิธีสร้างออบเจ็กต์และจัดเก็บในช่อง userInfo
ของ CPManuevers
สิ่งนี้จะเป็นตัวกำหนด CPManeuverDisplayStyle
ซึ่งใช้สำหรับข้อมูลคำแนะนำช่องทางด้วย ดูข้อมูลเพิ่มเติมได้ที่คู่มือการเขียนโปรแกรม
แอป CarPlay ของ Apple
Swift
// Get a CPTravelEstimate from GMSNavigationNavInfo
func getTravelEstimates(from navInfo:GMSNavigationNavInfo) -> CPTravelEstimates {
let distanceRemaining = navInfo.roundedDistance(navInfo.distanceToCurrentStepMeters)
let timeRemaining = navInfo.roundedTime(navInfo.timeToCurrentStepSeconds)
let travelEstimates = CPTravelEstimates(distanceRemaining: distanceRemaining, timeRemaining: timeRemaining)
return travelEstimates
}
// Create an object to be stored in the userInfo field of CPManeuver to determine the CPManeuverDisplayStyle.
/** An object to be stored in the userInfo field of a CPManeuver. */
struct ManeuverUserInfo {
var stepInfo: GMSNavigationStepInfo
var isLaneGuidance: Bool
}
func mapTemplate(_ mapTemplate: CPMapTemplate, displayStyleFor maneuver: CPManeuver) -> CPManeuverDisplayStyle {
let userInfo = maneuver.userInfo
if let maneuverUserInfo = userInfo as? ManeuverUserInfo {
return maneuverUserInfo.isLaneGuidance ? .symbolOnly : .leadingSymbol
}
return .leadingSymbol
}
// Get a CPManeuver with instructionVariants and symbolImage from GMSNavigationStepInfo
func getManeuver(for stepInfo: GMSNavigationStepInfo) -> CPManeuver {
let maneuver = CPManeuver()
maneuver.userInfo = ManeuverUserInfo(stepInfo: stepInfo, isLaneGuidance: false)
switch stepInfo.maneuver {
case .destination:
maneuver.instructionVariants = ["Your destination is ahead."]
break
case .destinationLeft:
maneuver.instructionVariants = ["Your destination is ahead on your left."]
break
case .destinationRight:
maneuver.instructionVariants = ["Your destination is ahead on your right."]
break
default:
maneuver.attributedInstructionVariants = currentNavInfo?.instructions(forStep: stepInfo, options: instructionOptions)
break
}
maneuver.symbolImage = stepInfo.maneuverImage(with: instructionOptions.imageOptions)
return maneuver
}
// Get the lane image for a CPManeuver from GMSNavigationStepInfo
func laneGuidanceManeuver(for stepInfo: GMSNavigationStepInfo) -> CPManeuver? {
let maneuver = CPManeuver()
maneuver.userInfo = ManeuverUserInfo(stepInfo: stepInfo, isLaneGuidance: true)
let lanesImage = stepInfo.lanesImage(with: imageOptions)
guard let lanesImage = lanesImage else { return nil }
maneuver.symbolImage = lanesImage
return maneuver
}
Objective-C
// Get a CPTravelEstimate from GMSNavigationNavInfo
- (nonull CPTravelEstimates *)travelEstimates:(GMSNavigationNavInfo *_Nonnull navInfo) {
NSMeasurement<NSUnitLength *> *distanceRemaining = [navInfo roundedDistance:navInfo.distanceToCurrentStepMeters];
NSTimeInterval timeRemaining = [navInfo roundedTime:navInfo.timeToCurrentStepSeconds];
CPTravelEstimate* travelEstimate = [[CPTravelEstimates alloc] initWithDistanceRemaining:distanceRemaining
timeRemaining:timeRemaining];
}
// Create an object to be stored in the userInfo field of CPManeuver to determine the CPManeuverDisplayStyle.
/** An object to be stored in the userInfo field of a CPManeuver. */
@interface ManeuverUserInfo : NSObject
@property(nonatomic, readonly, nonnull) GMSNavigationStepInfo *stepInfo;
@property(nonatomic, readonly, getter=isLaneGuidance) BOOL laneGuidance;
- (nonnull instancetype)initWithStepInfo:(GMSNavigationStepInfo *)stepInfo
isLaneGuidance:(BOOL)isLaneGuidance NS_DESIGNATED_INITIALIZER;
- (instancetype)init NS_UNAVAILABLE;
@end
- (CPManeuverDisplayStyle)mapTemplate:(CPMapTemplate *)mapTemplate
displayStyleForManeuver:(nonnull CPManeuver *)maneuver {
ManeuverUserInfo *userInfo = maneuver.userInfo;
return userInfo.laneGuidance ? CPManeuverDisplayStyleSymbolOnly : CPManeuverDisplayStyleDefault;
}
// Get a CPManeuver with instructionVariants and symbolImage from GMSNavigationStepInfo
- (nonnull CPManeuver *)maneuverForStep:(nonnull GMSNavigationStepInfo *)stepInfo {
CPManeuver *maneuver = [[CPManeuver alloc] init];
maneuver.userInfo = [[ManeuverUserInfo alloc] initWithStepInfo:stepInfo isLaneGuidance:NO];
switch (stepInfo.maneuver) {
case GMSNavigationManeuverDestination:
maneuver.instructionVariants = @[ @"Your destination is ahead." ];
break;
case GMSNavigationManeuverDestinationLeft:
maneuver.instructionVariants = @[ @"Your destination is ahead on your left." ];
break;
case GMSNavigationManeuverDestinationRight:
maneuver.instructionVariants = @[ @"Your destination is ahead on your right." ];
break;
default: {
maneuver.attributedInstructionVariants =
[_currentNavInfo instructionsForStep:stepInfo options:_instructionOptions];
break;
}
}
maneuver.symbolImage = [stepInfo maneuverImageWithOptions:_instructionOptions.imageOptions];
return maneuver;
}
// Get the lane image for a CPManeuver from GMSNavigationStepInfo
- (nullable CPManeuver *)laneGuidanceManeuverForStep:(nonnull GMSNavigationStepInfo *)stepInfo {
CPManeuver *maneuver = [[CPManeuver alloc] init];
maneuver.userInfo = [[ManeuverUserInfo alloc] initWithStepInfo:stepInfo isLaneGuidance:YES];
UIImage *lanesImage = [stepInfo lanesImageWithOptions:_imageOptions];
if (!lanesImage) {
return nil;
}
maneuver.symbolImage = lanesImage;
return maneuver;
}
การฝึกซ้อม
CarPlay ใช้คลาสCPManeuver
เพื่อให้คำแนะนำแบบเลี้ยวต่อเลี้ยว ดูเปิดใช้ฟีดข้อมูลแบบเลี้ยวต่อเลี้ยวสำหรับข้อมูลเพิ่มเติมเกี่ยวกับการขับขี่และคำแนะนำช่องทาง