تفعيل التنقّل في CarPlay

يوضّح هذا القسم كيفية استخدام حزمة تطوير البرامج Navigation SDK مع مكتبة Apple CarPlay لعرض تجربة التنقّل في تطبيقك على وحدات شاشة الرأس المدمجة في لوحة القيادة. إذا كان نظام شاشة السيارة يتيح استخدام CarPlay، يمكن للسائقين استخدام تطبيقك مباشرةً على شاشة السيارة من خلال توصيل هواتفهم بالوحدة. يتم أيضًا تشغيل الإرشاد الصوتي على مكبّرات صوت السيارة.

يمكنك إنشاء تطبيق CarPlay من مجموعة من نماذج واجهة المستخدم التي تقدّمها Apple. يتحمّل تطبيقك مسؤولية اختيار النموذج الذي سيتم عرضه وتقديم البيانات المضمّنة فيه.

يعرض النظام المضمّن في لوحة البيانات العناصر التفاعلية الموافَق عليها من حيث السلامة كي تتمكّن السائق من التنقّل بأمان إلى وجهته بدون تشتيت انتباهه بشكلٍ مفرط. يمكنك أيضًا برمجة تطبيقك ليتمكّن السائق من التفاعل مع الميزات الخاصة بتطبيقك، مثل قبول الطلبات أو رفضها أو عرض الموقع الجغرافي للعميل على الخريطة. يمكن أيضًا برمجة تعديلات حالة الطلبات لتظهر في الوحدة داخل لوحة البيانات.

شاشة التنقّل في CarPlay والهاتف
تعرض الصورة اليمنى مثالاً على شاشة التنقّل في CarPlay. تعرض الصورة على يسار الصفحة شريط التنقّل نفسه كما يظهر على الهاتف.

ضبط إعدادات الجهاز

البدء باستخدام CarPlay

أولاً، اطّلِع على مستندات Apple:

إعداد حزمة تطوير البرامج (SDK) للتنقّل

  1. بعد قراءة مستندات Apple، ستكون مستعدًا للعمل مع حزمة تطوير البرامج (SDK) لنظام التنقّل.
  2. إعداد مشروعك إذا لم يسبق لك دمج حزمة تطوير البرامج (SDK) لنظام التنقّل في تطبيقك
  3. فعِّل خلاصة توجيهات TurnByTurn لتطبيقك.
  4. اختيارية: استخدِم رموزاً تم إنشاؤها من حزمة تطوير البرامج Navigation SDK.
  5. ارسم الخريطة باستخدام فئة GMSMapView المقدَّمة في فئة UIView. اطّلِع على التنقّل في مسار للحصول على مزيد من المعلومات. املأ CPNavigationSession بالبيانات من مكتبة TurnByTurn.

رسم واجهة مستخدِم الخريطة والتنقّل

تعرِض فئة GMSMapView خريطة، بينما تعرِض فئة CPMapTemplate واجهة المستخدم على شاشات 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 في حزمة SDK لنظام التنقّل في تقديم بعض هذه المعلومات، مثل الرمز والنص والوقت المتبقّي.

إعداد مستمع

اتّبِع تعليمات إعداد مستمع للأحداث في تفاصيل عن خلاصة data التي تتضمّن بيانات التنقّل بالتفصيل.

تعبئة معلومات التنقّل

يوضّح الجزء الأول من نموذج الرمز البرمجي التالي كيفية إنشاء estimtimates للسفر في 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 الفئة لتقديم توجيهات بالتفصيل. اطّلِع على تفاصيل عن خلاصة بيانات التوجيهات المتعلّقة بالانعطافات للحصول على مزيد من المعلومات عن المناورات وإرشادات المسارات.