ส่วนนี้จะอธิบายวิธีใช้ Navigation SDK กับไลบรารี Apple CarPlay เพื่อแสดงประสบการณ์การนำทางของแอปในยูนิตหลักในแดชบอร์ด หากระบบในแผงหน้าปัดของคนขับรองรับ CarPlay คนขับจะใช้แอปของคุณบนจอแสดงผลของรถยนต์ได้โดยตรงด้วยการเชื่อมต่อโทรศัพท์กับยูนิต การนำทางด้วยเสียงจะทำงานบนลำโพงของรถด้วย
คุณสร้างแอป CarPlay จากชุดเทมเพลต UI ที่ Apple จัดเตรียมไว้ให้ แอปของคุณมีหน้าที่เลือกเทมเพลตที่จะแสดงและระบุข้อมูล ภายในเทมเพลต
ระบบในแดชบอร์ดจะแสดงองค์ประกอบแบบอินเทอร์แอกทีฟที่ผ่านการรับรองด้านความปลอดภัย เพื่อให้ผู้ขับขี่นำทางไปยังจุดหมายได้อย่างปลอดภัยโดยไม่ถูกรบกวนมากเกินไป นอกจากนี้ คุณยังตั้งโปรแกรมแอปเพื่อให้คนขับโต้ตอบกับฟีเจอร์เฉพาะของแอปได้ด้วย เช่น การยอมรับหรือปฏิเสธคำสั่งซื้อ หรือการดูตำแหน่งของลูกค้าบนแผนที่ นอกจากนี้ คุณยังตั้งโปรแกรมให้การอัปเดตสถานะคำสั่งซื้อปรากฏในหน่วยในแดชบอร์ดได้ด้วย
 
ตั้งค่า
เริ่มต้นด้วย CarPlay
ก่อนอื่น โปรดทำความคุ้นเคยกับเอกสารประกอบของ Apple
ตั้งค่า Navigation SDK
- เมื่ออ่านเอกสารประกอบของ Apple แล้ว คุณก็พร้อมที่จะใช้ Navigation SDK
- ตั้งค่าโปรเจ็กต์หากยังไม่ได้ผสานรวม Navigation SDK เข้ากับแอป
- เปิดใช้ฟีดคำแนะนำแบบเลี้ยวต่อเลี้ยว สำหรับแอป
- ไม่บังคับ ใช้ไอคอนที่สร้างขึ้น จาก Navigation SDK
- วาดแผนที่โดยใช้คลาส GMSMapViewที่ระบุไว้ในคลาส UIView ดูข้อมูลเพิ่มเติมได้ที่นำทางตามเส้นทาง ใส่ข้อมูลจากไลบรารี TurnByTurn ลงในCPNavigationSession
วาด 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 ใน Navigation SDK ช่วยให้ข้อมูลบางอย่างได้ เช่น สัญลักษณ์ ข้อความ และเวลาที่เหลือ
ตั้งค่าผู้ฟัง
ทำตามวิธีการตั้งค่าเครื่องมือฟังเหตุการณ์ในรายละเอียดเกี่ยวกับฟีดข้อมูล การนำทาง แบบเลี้ยวต่อเลี้ยว
ป้อนข้อมูลการนำทาง
ส่วนแรกของตัวอย่างโค้ดต่อไปนี้แสดงวิธีสร้างการประมาณเวลาเดินทางของ 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
คลาสเพื่อแสดง
คำแนะนำแบบเลี้ยวต่อเลี้ยว ดูข้อมูลเพิ่มเติมเกี่ยวกับ
ฟีดรายละเอียดเกี่ยวกับข้อมูลการนำทางแบบเลี้ยวต่อเลี้ยวเกี่ยวกับเลี้ยวและคำแนะนำเกี่ยวกับเลน
เอกสารประกอบที่เกี่ยวข้อง
- https://developer.apple.com/carplay/
- https://developer.apple.com/carplay/documentation/CarPlay-App-Programming-Guide.pdf
- https://developer.apple.com/design/human-interface-guidelines/carplay
- รายละเอียดเกี่ยวกับฟีดข้อมูลแบบเลี้ยวต่อเลี้ยว
- นำทางตามเส้นทาง