Linking في React Native

من موسوعة حسوب


خاصٌّ بالمشاريع المكتوبة بشفرة Native تطبّق هذه المقالة على المشاريع المكتوبة بشفرة Native فقط؛ أمّا عند استخدام سير العمل expo-cli، فيمكن الاطّلاع على الدّليل Linking ضمن توثيق Expo لإيجاد البديل الملائم.

تُقدّم Linking واجهةً عامّةً للتّفاعل مع الرّوابط الواردة، والصّادرة للتّطبيقات، ولكل رابط (URL) مخطّط URL، مثل: البادئة http، أو https في عنوان الموقع، كما يوجد المخطّط mailto، الذي يقوم نظام التّشغيل -عند فتح رابطٍ يحويه- بفتح تطبيق البريد الإكترونيّ المنصّب على الجهاز، كما توجد مخطّطاتٌ للاتّصال الهاتفيّ، وإرسال الرّسائل النّصّيّة، ستُشرح مخطّطات الرّوابط الضّمنيّة built-in URL لاحقًا بالتّفصيل.

يمكن الرّبط مع التّطبيقات الأخرى -بشكلٍ مماثلٍ للمخطّط mailto- باستخدام مخطّطات URL المخصصة، مثلًا: عند استلام البريد الإلكتروني Magic Link من تطبيق Slack سيكون المفتاح Launch Slack هو وسم الربط (anchor tag)، والذي يتضمّن رابطًا مرجعيّا فائقًا (href) له الشّكل التّالي: slack://secret/magic-login/other-secretيمكننا إخبار نظلم التشغيل بأننا نريد معالجة مخطط مخصّص كما في Slack، عند فتح تطبيق Slack فإنه يستقبل الرّابط المُستخدَم في فتحه، ويمكن بنفس الطّريقة ربط أيّ تطبيقٍ باستخدام المخطّطات المخصّصة، وهو ما يسمى بالرّبط العميق (deep linking) والذي سيُشرح لاحقًا بالتّفصيل.

مخطّطات URL المخصصة ليست الطّريقة الوحيدة لفتح التّطبيقات على الهاتف المحمول، حيث يفضّل عدم استخدمها مع روابط البريد الإلكتروني لأن ذلك سيؤدّي إلى تعطيلها على سطح المكتب، لكن يمكن استخدامها مع روابط https الاعتياديّة مثل: https://www.myapp.io/records/1234546يستخدم هذا الرّابط لفتح تطبيقٍ ما على الهاتف المحمول، وتدعى هذه الرّوابط بالروابط العميقة (Deep Links) على منصّة Android؛ أمّا على منصّة iOS فتدعى بالرّوابط الشّاملة (Universal Links).

مخطّطات الرّوابط الضّمنيّة

إن هذه المخطّطات من الوظائف الأساسيّة، وتتوافر في الأنظمة كلّها، وأكثرها استخدامًا:

المخطط الوصف iOS Android
mailto فتح تطبيق البريد الإلكتروني، مثل : mailto: support@expo.io
tel فتح نافذة الاتصال للاتصال بالرقم المعطى، مثل: tel:+123456789
sms فتح تطبيق الرسائل النصية، مثل: sms:+123456789
https / http فتح متصفح الويب مثل: expo.io

تفعيل الرّوابط العميقة

على منصّة Android

لمعرفة كيفيّة دعم الرّبط العميق على منصّة Android يمكن زيارة تفعيل الروابط العميقة لمحتوى التطبيق - إضافة الفلاتر إليها.

عند الرّغبة باستقبال نيّة الفعل (intent) على النسخة الموجودة من MainActivity، يجب ضبط launchMode الخاصّ بنسخة MainActivity للقيمة singleTask في AndroidManifest.xml، ويمكن الاطّلاع على <activity> لمزيدٍ من المعلومات.

<activity
  android:name=".MainActivity"
  android:launchMode="singleTask">

على منصّة IOS

ملاحظة: يجب ربط RCTLinking مع المشروع وفق الخطوات المشروحة هنا، ويجب إضافة السّطور البرمجيّة التاليّة للملف ‎*AppDelegate.m عند الرغبة بالتّنصّت على روابط التّطبيق الواردة أثناء فترة عمل هذا التّطبيق :

// iOS 9.x or newer
#import <React/RCTLinkingManager.h>

- (BOOL)application:(UIApplication *)application
   openURL:(NSURL *)url
   options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
{
  return [RCTLinkingManager application:application openURL:url options:options];
}

أمّا في منصّة IOS ذات الإصدار 8.x، أو الأقدم منه، فتستخدم السّطور البرمجيّة التّالية عوضًا عن الأسطر السّابقة:

// iOS 8.x or older
#import <React/RCTLinkingManager.h>

- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url
  sourceApplication:(NSString *)sourceApplication annotation:(id)annotation
{
  return [RCTLinkingManager application:application openURL:url
                      sourceApplication:sourceApplication annotation:annotation];
}

يجب إضافة السّطور البرمجيّة التّالية أيضًا، إذا استخدم التطبيق الرّوابط الشّاملة (Universal Links):

- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity
 restorationHandler:(nonnull void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler
{
 return [RCTLinkingManager application:application
                  continueUserActivity:userActivity
                    restorationHandler:restorationHandler];
}

معالجة الرّوابط العميقة

تعالَج الرّوابط URL التي تفتح التّطبيقات بطريقتين:

  • إذا كان التّطبيق مفتوحًا مسبقًا، فإنه يُجلَب للواجهة، ويُطلق حدث الربط، باستخدام التّابع ‫‪‎‏‪ ‎‏.‪Linking.addEventListener(url, callback)
  • إذا كان التطبيق يفتَح للمرّة الأولى، فإنّه يُفتح، ويُمرَّر الرّابط url كرابطٍ مبدئيٍّ InitialURL، باستخدام التّابع ()Linking.getInitialURL الذي يعيد وعداً (Promise) يُقبل كرابط url -عند وجوده-.

مثالٌ

فتح الرّوابط الضّمنيّة والعميقة (والشّاملة)

import React, { useCallback } from "react";
import { Alert, Button, Linking, StyleSheet, View } from "react-native";

const supportedURL = "https://google.com";

const unsupportedURL = "slack://open?team=123456";

const OpenURLButton = ({ url, children }) => {
  const handlePress = useCallback(async () => {
    // Checking if the link is supported for links with custom URL scheme.
    const supported = await Linking.canOpenURL(url);

    if (supported) {
      // Opening the link with some app, if the URL scheme is "http" the web link should be opened
      // by some browser in the mobile
      await Linking.openURL(url);
    } else {
      Alert.alert(`Don't know how to open this URL: ${url}`);
    }
  }, [url]);

  return <Button title={children} onPress={handlePress} />;
};

const App = () => {
  return (
    <View style={styles.container}>
      <OpenURLButton url={supportedURL}>Open Supported URL</OpenURLButton>
      <OpenURLButton url={unsupportedURL}>Open Unsupported URL</OpenURLButton>
    </View>
  );
};

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: "center", alignItems: "center" },
});

export default App;

فتح الإعدادات الشّخصيّة

import React, { useCallback } from "react";
import { Button, Linking, StyleSheet, View } from "react-native";

const OpenSettingsButton = ({ children }) => {
  const handlePress = useCallback(async () => {
    // Open the custom settings if the app has one
    await Linking.openSettings();
  }, []);

  return <Button title={children} onPress={handlePress} />;
};

const App = () => {
  return (
    <View style={styles.container}>
      <OpenSettingsButton>Open Settings</OpenSettingsButton>
    </View>
  );
};

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: "center", alignItems: "center" },
});

export default App;

الحصول على الرّابط العميق

import React, { useState, useEffect } from "react";
import { Linking, StyleSheet, Text, View } from "react-native";

const useMount = func => useEffect(() => func(), []);

const useInitialURL = () => {
  const [url, setUrl] = useState(null);
  const [processing, setProcessing] = useState(true);

  useMount(() => {
    const getUrlAsync = async () => {
      // Get the deep link used to open the app
      const initialUrl = await Linking.getInitialURL();

      // The setTimeout is just for testing purpose
      setTimeout(() => {
        setUrl(initialUrl);
        setProcessing(false);
      }, 1000);
    };

    getUrlAsync();
  });

  return { url, processing };
};

const App = () => {
  const { url: initialUrl, processing } = useInitialURL();

  return (
    <View style={styles.container}>
      <Text>
        {processing
          ? `Processing the initial url from a deep link`
          : `The deep link is: ${initialUrl || "None"}`}
      </Text>
    </View>
  );
};

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: "center", alignItems: "center" },
});

export default App;

إرسال نيّة الفعل (intent) (على منصّة Android)

import React, { useCallback } from "react";
import { Alert, Button, Linking, StyleSheet, View } from "react-native";

const SendIntentButton = ({ action, extras, children }) => {
  const handlePress = useCallback(async () => {
    try {
      await Linking.sendIntent(action, extras);
    } catch (e) {
      Alert.alert(e.message);
    }
  }, [action, extras]);

  return <Button title={children} onPress={handlePress} />;
};

const App = () => {
  return (
    <View style={styles.container}>
      <SendIntentButton action="android.intent.action.POWER_USAGE_SUMMARY">
        Power Usage Summary
      </SendIntentButton>
      <SendIntentButton
        action="android.settings.APP_NOTIFICATION_SETTINGS"
        extras={[
          { "android.provider.extra.APP_PACKAGE": "com.facebook.katana" },
        ]}
      >
        App Notification Settings
      </SendIntentButton>
    </View>
  );
};

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: "center", alignItems: "center" },
});

export default App;

التوابع

constructor()

constructor();

addEventListener()

addEventListener(type, handler);

يضيف معالجًا لتغيّرات الرّبط، بالتّنصّت على الحدث من نوع url، وتزويد المعالج.

removeEventListener()

removeEventListener(type, handler);

يزيل المعالج بتمرير الحدث من نوع url، والمعالج المطلوب.

openURL()

openURL(url);

يحاول فتح عنوان url المعطى بواحدٍ من التّطبيقات الموجودة.

يمكن استخدام عناوين URLs أخرى كموقع (مثل: "geo:37.484847،-122.148386" على منصّة Android، و"maps.apple " على منصّة iOS)، أو جهات الاتصال (contact)، أو أيّ عناوين يمكن فتحها بالتّطبيقات.

يعيد هذا التابع وعدًا Promise يقبل عند موافقة المستخدم على مربع الحوار المفتوح، أو إذا كان الرابط url يفتح آليًا، ويُرفض إذا ألغى المستخدم مربع الحوار، أو عند عدم وجود تطبيقٍ مُسجّلٍ للعنوان url.

المعاملات

الاسم النوع مطلوب الوصف
url سلسلة نصية (string) نعم الرابط الذي سيتم فتحه

تنبيه:

  • سيفشل هذا التابع، إذا لم يعلم كيفيّة فتح عنوان URL المحدّد، لذا يجب استخدام code canOpenURL@ عند تمرير عناوين URL ليست من البروتوكول ‪‪‎‏‪‫‪.http(s)
  • يجب وضع البروتوكول ("http://", "https://") بشكلٍ موافقٍ لعناوين URL الخاصّة بالويب.
  • قد يعمل هذا التابع بشكلٍ مختلفٍ على المحاكي (simulator)، فمثلًا: لا تعالج الرّوابط ":‫tel" على محاكي IOS بسبب عدم وجود وصولٍ إلى تطبيق الاتصال.

canOpenURL()

canOpenURL(url);

يقرر وجود (أو عدم وجود) تطبيقٍ على الجهاز ، يكون قادرًا على معالجة عنوان url المعطى.

يعيد هذا التابع وعدًا Promise يقبل عندما يقرر إمكانيّة (أو عدم إمكانيّة) معالجة العنوان url المعطى، ويكون معامله الأول هو إمكانية فتحه، ويرفَض هذا الوعد عندما لا يكون فحص إمكانيّة فتح url ممكنًا، كما يُرفض على منصّة IOS، إذا لم يوضع مخطّطٌ محدّدٌ في المفتاح LSApplicationQueriesSchemes داخل الملف Info.plist.

المعاملات

الاسم النوع مطلوب الوصف
url سلسلة نصية (string) نعم الرابط الذي سيتم فتحه

تنبيه:

  • يجب وضع البروتوكول ("http://", "https://") بشكلٍ موافقٍ لعناوين URL الخاصّة بالويب.
  • في منصّة IOS ذات الإصدار 9، يجب وضع مخطّطٍ محدّدٍ في المفتاح LSApplicationQueriesSchemes داخل الملف Info.plist، وإلّا سيعيد هذا التابع القيمة false دائمًا.
  • يعمل هذا التابع بشكلٍ محدودٍ على منصّة IOS ذات الإصدار 9+، حسب الموقع الرسميّ لـ the official Apple documentation.
  • يمكن تكرار استدعاء هذا التابع حتى 50 مرّةٍ عند ربط التطبيق على إصدارات IOS الأقدم من 9، وتشغيله على الإصدار 9، وما بعده. فإذا تجاوز عدد مرات استدعائه الحدّ المسموح به، فسيعيد القيمة false دومًا، ويمكن تصفير الحدّ المسموح عن طريق إعادة تنصيب التّطبيق، أو تحديثه.

openSettings()

openSettings();

فتح تطبيق الإعدادات (Settings) لإظهار الإعدادات الشّخصيّة للتّطبيقات إن وُجدت.

getInitialURL()

getInitialURL();

يعيد هذا التابع عنوان الرابط url، إذا أطلِق التّطبيق عن طريق رابطه، وإلا سيعيد null.

ملاحظة: لدعم الرّبط العميق على منصّة Android، يرجى زيارة deep-linking، على developer.android.

sendIntent()

sendIntent(action: string, extras?: Array<{key: string, value: string | number | boolean}>)

‏‏‎@platform android

يعمل على منصّة Android فقط، ويستخدم لإطلاق نيّة الفعل (intent) مع الإضافات (اختيارية).

مصادر