เปิดใช้การนำทางสำหรับ CarPlay

ส่วนนี้จะอธิบายวิธีใช้ Navigation SDK กับไลบรารี Apple CarPlay เพื่อแสดงประสบการณ์การนำทางของแอปในจอภาพในหน้าแดชบอร์ด หากระบบในแดชบอร์ดของผู้ขับขี่รองรับ CarPlay ผู้ขับขี่จะใช้แอปของคุณบนจอแสดงผลของรถได้โดยตรงโดยเชื่อมต่อโทรศัพท์กับระบบ เสียงบรรยายจะเล่นผ่านลําโพงของรถด้วย

คุณสร้างแอป CarPlay จากชุดเทมเพลต UI ที่ Apple มีให้ แอปของคุณมีหน้าที่รับผิดชอบในการเลือกเทมเพลตที่จะแสดงและระบุข้อมูลภายในเทมเพลต

ระบบในแดชบอร์ดจะแสดงองค์ประกอบแบบอินเทอร์แอกทีฟที่ผ่านการรับรองด้านความปลอดภัยเพื่อให้ผู้ขับขี่ไปยังจุดหมายได้อย่างปลอดภัยโดยไม่ถูกรบกวนโดยไม่จำเป็น นอกจากนี้ คุณยังตั้งโปรแกรมแอปเพื่อให้คนขับโต้ตอบกับฟีเจอร์เฉพาะของแอปได้ เช่น ยอมรับหรือปฏิเสธคำสั่งซื้อ หรือดูตำแหน่งของลูกค้าบนแผนที่ นอกจากนี้ คุณยังตั้งโปรแกรมการอัปเดตสถานะคำสั่งซื้อให้ปรากฏในแดชบอร์ดได้ด้วย

การนำทางของ CarPlay และโทรศัพท์จะแสดงขึ้น
รูปภาพด้านซ้ายแสดงตัวอย่างจอแสดงผลการนำทางของ CarPlay รูปภาพด้านขวาแสดงการนําทางแบบเดียวกับที่ปรากฏในโทรศัพท์

ตั้งค่า

เริ่มต้นด้วย CarPlay

ก่อนอื่น ให้ทำความคุ้นเคยกับเอกสารประกอบของ Apple ดังนี้

ตั้งค่า Navigation SDK

  1. เมื่ออ่านเอกสารประกอบของ Apple จนจบแล้ว คุณก็พร้อมที่จะใช้งาน Navigation SDK
  2. ตั้งค่าโปรเจ็กต์หากยังไม่ได้ผสานรวม Navigation SDK เข้ากับแอป
  3. เปิดใช้ฟีดคำแนะนำแบบเลี้ยวต่อเลี้ยวสำหรับแอป
  4. ไม่บังคับ ใช้ไอคอนที่สร้างขึ้นจาก Navigation SDK
  5. วาดแผนที่โดยใช้คลาส 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 จะเริ่มทำงาน

แสดงคำแนะนำการนำทาง

ส่วนนี้จะอธิบายวิธีตั้งค่าโปรแกรมรับฟังสําหรับฟีดข้อมูลและวิธีป้อนข้อมูลเส้นทางในการนําทางในแผงคําแนะนําและแผงระยะเวลาการเดินทางโดยประมาณ ดูข้อมูลเพิ่มเติมได้ที่ส่วน"สร้างแอปการนำทาง CarPlay" ในคู่มือการเขียนโปรแกรมแอป CarPlay

แผงคำแนะนำและแผงเวลาโดยประมาณของการเดินทางจะมีการ์ดการนำทางที่แสดงข้อมูลการนำทางที่เกี่ยวข้องกับการเดินทางปัจจุบัน ไลบรารี TurnByTurn ใน Navigation SDK จะช่วยระบุข้อมูลบางอย่าง เช่น สัญลักษณ์ ข้อความ และเวลาที่เหลือ

ตั้งค่าโปรแกรมฟัง

ทำตามวิธีการตั้งค่าโปรแกรมรับฟังเหตุการณ์ในรายละเอียดเกี่ยวกับฟีดข้อมูลคำแนะนำแบบเลี้ยวต่อเลี้ยว

ป้อนข้อมูลการนำทาง

ส่วนแรกของตัวอย่างโค้ดต่อไปนี้แสดงวิธีสร้างเวลาเดินทางโดยประมาณใน CarPlay ด้วยการแปล GMSNavigationNavInfo.timeToCurrentStepSeconds เป็น CPTravelEstimate คุณสามารถอ่านข้อมูลเพิ่มเติมเกี่ยวกับองค์ประกอบการแสดงผลเหล่านี้และองค์ประกอบอื่นๆ ได้ในรายละเอียดเกี่ยวกับฟีดข้อมูลการนําทางแบบเลี้ยวต่อเลี้ยว

ส่วนที่สองของตัวอย่างแสดงวิธีสร้างออบเจ็กต์และจัดเก็บไว้ในช่อง 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 class เพื่อแสดงคำแนะนำแบบเลี้ยวต่อเลี้ยว ดูข้อมูลเพิ่มเติมเกี่ยวกับคำแนะนำเลนและการเปลี่ยนเลี้ยวได้ที่รายละเอียดเกี่ยวกับฟีดข้อมูลการเลี้ยวต่อเลี้ยว