تُرجمت هذه الصفحة آليًا وقد تحتوي على أخطاء. اقرأ الأصل الإنجليزي

التوثيق

iOS

تعمل تطبيقات NSH على iOS باستخدام عروض UIKit ونصوص CoreText، على الأجهزة وفي المحاكي، بدءًا من iOS 15.0. تعمل التطبيقات الكاملة وتُحزَّم كما هو الحال على المنصات الأخرى، ويمكن لتطبيق موجود عرض تطبيقات NSH داخل عروضه الخاصة، سواء كان جذر Rust يربطه أو TSX يحمّله، بما في ذلك الحزم الموقّعة التي ينزّلها، ضمن حدود وصلاحيات يحددها. على iOS، os() هو Os::Ios (وplatform.os هو "ios" في TSX) وتسري خصائص ios={{…}} في TSX.

#المتطلبات

  • Xcode على جهاز Mac، مع بيئة تشغيل لمحاكي iOS (يثبّت xcodebuild -downloadPlatform iOS واحدة). راجع التثبيت.
  • تحتاج التطبيقات إلى iOS 15.0 أو أحدث، على iPhone وiPad.

#تشغيل تطبيق

يعمل تطبيق Rust من مجلده، أما تطبيق TSX فتحدد له ملف الدخول:

nsh run --target ios                                  # a Rust app
nsh run --target ios --device "iPhone 16 Pro"
nsh run App.tsx --target ios                          # a TSX app

يستخدم nsh محاكيًا قيد التشغيل، أو يشغّل أحدث iPhone. بالنسبة إلى تطبيق Rust، يبني المشغّل الخاص بالتطبيق مع الـ crate الخاصة بك، ويثبّته ويعرض سجلاته في الطرفية؛ وأي تغيير في كود Rust الخاص بالـ crate يعيد البناء ويعيد تشغيل التطبيق. بالنسبة إلى تطبيق TSX، يثبّت مضيف التطوير ويُبقي التطبيق محدّثًا أثناء الحفظ، مع عرض السجلات في الطرفية. يختار --device محاكيًا بالاسم أو UDID. للحصول على حلقة تطوير أسرع دون محاكٍ، يفتح nsh run --target catalyst على جهاز Mac واجهة UIKit الخلفية نفسها في نافذة Mac Catalyst بحجم iPhone.

#التحزيم

nsh package --target ios-simulator --run      # .nsh/out/<Name>.app, installed and launched in the simulator
nsh package --target ios --sign "Apple Distribution: Example Inc (ABCDE12345)" --profile AppStore.mobileprovision

من مجلد تطبيق Rust، يكون التطبيق هو مشغّل iOS المبني مع الـ crate الخاصة بك؛ أما تطبيق TSX فتحدد له ملف الدخول (nsh package App.tsx --target ios-simulator --run).

يكتب --target ios ملف .ipa موقّعًا للأجهزة أو TestFlight أو App Store. لمعرفة هويات التوقيع وملفات التزويد والأيقونات: التحزيم.

#التضمين في تطبيق iOS

يمكن لتطبيق iOS موجود عرض تطبيقات NSH داخل واجهته الخاصة. يربط تطبيق iOS الذي يحتوي على كود Rust الـ crate الخاصة بك والـ crate nsh-platform-ios ويعرض الجذر الخاص بك في AppView (تضمين تطبيق Rust). أما تطبيق Swift فيربط NativeShell.xcframework، وهو framework يتضمن وحدة Swift: تنشئ NshAppView (وهو UIView)، وتضيفه في أي مكان ضمن تسلسل العروض الهرمي، ثم تحمّل فيه تطبيق TSX أو حزمة موقّعة. تتعايش شاشاتك الأصلية والتنقل وتطبيق NSH جنبًا إلى جنب في تسلسل UIKit هرمي واحد.

كل عرض هو محرك مستقل واحد؛ ولا توجد كائنات singleton عامة، لذا يمكن لتطبيق ما عرض عدة تطبيقات NSH في آن واحد، على سبيل المثال تطبيق واحد لكل تبويب.

One host app with two NshAppView regions: each is its own engine with its own thread, VM, limits and capabilities; the host grants capabilities, sets limits and registers native components such as a map. One host app with two NshAppView regions: each is its own engine with its own thread, VM, limits and capabilities; the host grants capabilities, sets limits and registers native components such as a map.

#تضمين تطبيق Rust

nsh_platform_ios::AppView هو محرك واحد على هيئة UIView لتطبيق iOS مبني بـ Rust: يركّب مكوّن الجذر الخاص بالـ crate، دون أي JavaScript VM.

use nsh_platform_ios::{set_ui_sets, AppView, Root, SurfaceOptions};
use objc2::MainThreadMarker;
use objc2_ui_kit::UIView;

fn show(container: &UIView) -> AppView {
    // Once, before the first view: the UI sets the apps import (NSH's and your crate's).
    set_ui_sets(vec![nsh_base::registry(), my_app::registry()]);
    let mtm = MainThreadMarker::new().expect("main thread");
    let app = AppView::new(mtm, container.bounds(), SurfaceOptions::default());
    container.addSubview(app.view());
    app.run(Root::of::<my_app::App>());
    // Keep the AppView: dropping it stops the app.
    app
}

يتغير حجم العرض مع العرض الأعلى منه. AppView::run يستبدل ما كان يعمل؛ ويشغّل AppView::load حزمة TSX بدلًا منه (Bundle::source، أو unsafe { Bundle::bytecode(bytes) } لـ bundle.nshc داخل حزمة التطبيق). يحتوي SurfaceOptions على الحدود والصلاحيات الممنوحة ومجلد البيانات والمكوّنات الأصلية، كما تفعل أعضاء NshAppView أدناه؛ وتطابق AppView::set_locale وset_font_scale وset_error_handler كلًّا من locale وfontScale وonError. يبني nsh package --target ios هذا النوع من التطبيقات لك، مع تركيب الجذر الخاص بك.

#الحصول على الـ framework

تحتوي حزمة التوزيع على النسختين كلتيهما:

dist/nsh/embed/ios/NativeShell.xcframework      release: bundles and signed packages
dist/nsh/embed/ios/NativeShellDev.xcframework   also connects to a `nsh dev` server

ومن نسخة NSH المسحوبة (checkout) يمكنك بناء الـ frameworks فقط:

cargo xtask ios-embed    # writes dist/nsh/embed/ios/{NativeShell,NativeShellDev}.xcframework

ينتج البناء الافتراضي frameworks ثابتة. لبناء frameworks ديناميكية، استخدم:

cargo xtask ios-embed --dynamic

تمنح الـ frameworks الديناميكية وقت التشغيل صورة مكتبة وجدول فك مكدس (unwind table) خاصين به، مما يدعم التطبيقات المضيفة التي تربط بالفعل أوقات تشغيل Rust أخرى. اختر إعداد التضمين في Xcode أدناه بما يطابق الـ framework الذي تبنيه.

افتراضيًا، يحتوي كل XCFramework على ثلاث شرائح arm64: أجهزة iOS ومحاكي iOS (Apple silicon) وMac Catalyst. الحد الأدنى لهدف النشر هو iOS 15.0. تُترجَم وحدة Swift مع library evolution، ولذلك يمكن للتطبيقات المبنية بإصدارات أخرى من مترجم Swift استيرادها، ويربط الـ framework كل ما يحتاج إليه: فلا تضيف شيئًا آخر.

#إضافته إلى مشروع Xcode

  1. اسحب NativeShell.xcframework (أو NativeShellDev.xcframework لبنيات التطوير) إلى Frameworks, Libraries, and Embedded Content الخاص بهدف التطبيق. اختر Do Not Embed للـ frameworks الثابتة الافتراضية، أو Embed & Sign للـ frameworks الديناميكية المبنية بـ --dynamic.
  2. في Swift، import NativeShell.
  3. في Info.plist الخاص بالتطبيق، عيّن CADisableMinimumFrameDurationOnPhone إلى YES: على أجهزة iPhone التي تدعم ProMotion ستتحرك الرسوم المتحركة للتطبيق عندئذٍ بمعدل 120 Hz، كما يحدث في التمرير (التطبيقات التي يبنيها nsh package تتضمن ذلك).

#شحن حزمة مع التطبيق

يشحن تطبيق Swift تطبيقات TSX على هيئة حزم (bundles). ابنِ تطبيق NSH في مجلد وأضف هذا المجلد إلى موارد التطبيق كمرجع مجلد (folder reference) باسم nsh:

nsh build App.tsx --release --out MyApp/nsh

يكتب nsh build ما يلي:

الملفالمحتويات
bundle.nshcالتطبيق مترجمًا إلى bytecode: الأسرع تحميلًا.
bundle.jsالتطبيق نفسه على هيئة مصدر JS.
bundle.js.mapخريطة المصدر الخاصة به (تشير تتبّعات المكدس إلى ملفات .tsx الخاصة بك).
capabilities.jsonالصلاحيات التي يصرّح بها التطبيق، عندما يحتوي nsh.json على أي منها.
assets/الصور والخطوط التي يستوردها التطبيق، في مساراتها النسبية إلى ملف الدخول.

#عرض التطبيق

import NativeShell
import UIKit

final class EmbedController: UIViewController {
    private let app = NshAppView()

    override func viewDidLoad() {
        super.viewDidLoad()
        app.translatesAutoresizingMaskIntoConstraints = false
        app.onError = { message in print("nsh error: \(message)") }
        view.addSubview(app)
        NSLayoutConstraint.activate([
            app.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            app.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            app.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            app.bottomAnchor.constraint(equalTo: view.bottomAnchor),
        ])

        guard let url = Bundle.main.url(forResource: "bundle", withExtension: "nshc", subdirectory: "nsh") else { return }
        do {
            try app.loadBundle(at: url)
        } catch {
            print("cannot load: \(error)")
        }
    }

    // Let the embedded app's pages and dialogs decide the status bar.
    override var preferredStatusBarStyle: UIStatusBarStyle { NshAppView.preferredStatusBarStyle }
    override var prefersStatusBarHidden: Bool { NshAppView.prefersStatusBarHidden }
    override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
        NshAppView.preferredStatusBarUpdateAnimation
    }
}

حجم العرض هو نافذة التطبيق: يرتّب NSH التطبيق وفق حدود العرض ويعيد ذلك كلما تغيرت. يؤدي التحميل مرة أخرى إلى استبدال التطبيق الجاري؛ ويتوقف التطبيق عند تحرير العرض من الذاكرة.

العيّنة الكاملة، وهي تطبيق UIKit يعرض تطبيق NSH أسفل ترويسته الأصلية، هي examples/embed-ios/main.swift.

#NshAppView

NshAppView هو صنف فرعي من UIView معزول على الـ main actor.

العضوالوصف
init(frame: CGRect = .zero, trace: Bool = false)ينشئ العرض. trace يسجّل زمن كل commit. init?(coder:) يدعم storyboards وnibs.
onError: ((String) -> Void)?يتلقى أخطاء التطبيق القاتلة (حزمة يفشل تحميلها، واستثناءات JS غير المعالجة، وVM أوقفها حد معيّن) على الخيط الرئيسي. تُسجَّل الأخطاء دائمًا؛ وتعرضها بنيات التطوير أيضًا فوق التطبيق.
limits: NshLimitsحدود VM للتطبيقات المحمّلة من الآن فصاعدًا.
grantCapabilities(_ json: String?) throwsأقصى ما يُسمح للتطبيقات المحمّلة من الآن فصاعدًا بفعله، بصيغة JSON على شكل capabilities الخاص بـ nsh.json؛ ويمنح nil ما يصرّح به كل تطبيق. يرمي NshError عند JSON غير صالح.
locale: Locale?لغة التطبيق ومنطقته بدلًا من لغة المستخدم ومنطقته: تتبعها Intl وlocale() والمكوّنات فورًا، وتبدأ بها التطبيقات المحمّلة لاحقًا. nil يتبع إعدادات النظام.
fontScale: CGFloat?حجم النص بدلًا من حجم Dynamic Type (القيمة 1 هي الافتراضية): يُعاد ترتيب النصوص والمدخلات وفقه ويقرؤه fontScale()، فورًا وللتطبيقات المحمّلة لاحقًا؛ أما الأطوال فتحتفظ بحجمها. nil يتبع إعدادات النظام.
setThemeOverrides(_ json: String?) throwsهوية المضيف التجارية فوق سمة التطبيق: ملف سمة بصيغة "theme" الخاص بـ nsh.json. تتغلب رموزها (tokens) على رموز التطبيق ذات الأسماء نفسها، في الوضعين الفاتح والداكن، وتبقى رموز التطبيق الأخرى. يُطبَّق فورًا وعلى التطبيقات المحمّلة لاحقًا؛ ويعيد nil للتطبيق سمته الخاصة. يرمي NshError عند ملف لا يمكن تحليله.
storageDirectory: URL?المكان الذي تحفظ فيه التطبيقات المحمّلة من الآن فصاعدًا تخزينها؛ nil: Library/Application Support/nsh/<app id>، بحسب معرّف التطبيق نفسه.
cacheOptions: NshCacheOptionsالمكان الذي تحفظ فيه التطبيقات المحمّلة من الآن فصاعدًا ذاكرتها المؤقتة، والصور التي تنزّلها، وكم يحتفظ به كل منها: location .system (Library/Caches/nsh، كل تطبيق في <app id>)، أو .directory(URL) أو .off؛ images .default (256 MB)، أو .maxSize(megabytes:) أو .off.
loadBundle(at url: URL) throwsيشغّل حزمة كتبها nsh build. تأتي الأصول من assets/ المجاور لها؛ كما يُقرأ bundle.js.map وcapabilities.json المجاوران لها.
run(_ package: NshPackage)يشغّل حزمة موقّعة تم التحقق منها (انظر أدناه). تعمل الحزمة مرة واحدة.
loadPackage(at:trust:) async throws -> NshPackage.Infoيتحقق من الحزمة الموجودة في عنوان URL خارج الخيط الرئيسي، ثم يشغّلها. يرمي سبب الرفض؛ ويبقى التطبيق الجاري.
connectDevServer(_ url: URL) throwsللتطوير: يحمّل من خادم nsh dev ويعيد التحميل عند كل تغيير. يرمي خطأً مع NativeShell.xcframework، الذي لا يدعم التطوير.
register(component:_:)يسجّل مكوّنًا أصليًا.
params: [String: Any]معاملات الإطلاق للتطبيقات المحمّلة من الآن فصاعدًا: يقرؤها التطبيق عند البدء بصيغة host.params (host).
send(_ name: String, _ data: Any? = nil)يرسل حدثًا إلى التطبيق (host.on(name, …))؛ ويُحتفظ به إلى أن يستمع إليه، أو للتطبيق التالي ما دام لا يوجد تطبيق يعمل.
handle(_ name: String, _ handler:)يعالج استدعاءات التطبيق (host.call) ورسائله المرسلة (host.post) الخاصة بـ name على الخيط الرئيسي: { data, reply in … }، أو دالة async throws { data in … } تجيب نتيجتها عن الاستدعاء.
removeHandler(_ name: String)يزيل معالج name: تفشل استدعاءات التطبيق له فورًا.
static preferredStatusBarStyle، prefersStatusBarHidden، preferredStatusBarUpdateAnimationما طلبته التطبيقات العاملة (statusBar الخاص بصفحاتها وحواراتها، والقيمة الافتراضية لـ nsh.json): أعدها من overrides الخاصة بـ view controller المستضيف.

تسري limits وgrantCapabilities وstorageDirectory وcacheOptions ابتداءً من التحميل التالي: عيّنها قبل loadBundle / run.

يحتفظ كل تطبيق بتخزينه تحت معرّفه الخاص دون أي إعداد: الحزمة الموقّعة تحت المعرّف الذي تضمنه شهادة مفتاح إصدارها، والحزمة العادية تحت id الخاص بـ nsh.json (app.json المجاور لها، ويكتبه nsh build). عيّن storageDirectory لحفظ بيانات تطبيق في مكان يختاره المضيف:

app.storageDirectory = FileManager.default
    .urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
    .appendingPathComponent("nsh/com.example.orders")
try app.loadBundle(at: url)

#التخاطب مع التطبيق

يتخاطب العرض والتطبيق الذي يشغّله عبر قناة خاصة بهما (host): معاملات إطلاق يقرؤها التطبيق عند البدء، وأحداث من التطبيق المضيف، واستدعاءات ورسائل من التطبيق، ببيانات JSON (كائنات Foundation في هذا الجانب). لا تحتاج إلى أي عرض في التطبيق ولا إلى مكوّن أصلي.

// Before loading: what the app opens first (`host.params` in TSX).
app.params = ["conversation": notification.conversationID]
try app.loadBundle(at: url)

// The app's `await host.call("user.profile", { id })`: return the answer, or throw to reject it.
app.handle("user.profile") { data in
    let id = (data as? [String: Any])?["id"] as? Int ?? 0
    let profile = try await api.profile(id: id)
    return ["id": id, "name": profile.name]
}

// A handler that answers later, from any thread, with its reply.
app.handle("photo.pick") { _, reply in
    picker.present { url in
        if let url { reply.resolve(url.absoluteString) } else { reply.reject("cancelled") }
    }
}

// The app's `host.post("analytics", …)`: no answer.
app.handle("analytics") { data, _ in analytics.log(data) }

// Later: the app hears `host.on("conversation.open", …)`.
app.send("conversation.open", ["id": tapped.conversationID])

تعمل المعالجات على الخيط الرئيسي؛ وينتظر الاستدعاء إجابته داخل التطبيق دون حجب أي من الطرفين. الرد الذي يُحرَّر دون إجابة يُفشل الاستدعاء، والاستدعاء باسم لا معالج له يفشل فورًا. تُحفظ الأحداث ذات الأسماء التي لا يستمع إليها التطبيق بعد إلى أن يستمع إليها، وتذهب الأحداث المرسلة بينما لا يعمل أي تطبيق (قبل loadBundle، وأثناء إعادة التحميل) إلى التطبيق التالي، بحيث يصل إليه الإشعار الذي نُقر عليه أثناء بدء التطبيق.

#تجاوزات السمة

عندما يُضمَّن تطبيق واحد في عدة تطبيقات مضيفة ذات هويات تجارية مختلفة، يتبنى هوية كل مضيف: يعطي المضيف الرموز التي تغيّرها هويته، على هيئة ملف سمة بصيغة "theme" الخاص بـ nsh.json (cssVars الخاص بـ shadcn/ui، وقيمه كما تكتبها CSS). وهي تتغلب على رموز التطبيق ذات الأسماء نفسها؛ وتبقى رموز التطبيق الأخرى. أعطِ الرمز قيمته للوضع الداكن تحت dark (المجموعة الواحدة فقط تحت light أو theme هي ذلك اللون في الوضعين):

try app.setThemeOverrides("""
{ "cssVars": {
    "theme": { "radius": "0.75rem" },
    "light": { "primary": "#e11d48", "primary-foreground": "#ffffff" },
    "dark": { "primary": "#fb7185", "primary-foreground": "#1c1917" }
} }
""")
try app.loadBundle(at: url)

استدعِه قبل التحميل أو بعده: تعيد أصناف مثل bg-primary وrounded-(--radius) تنسيقها فورًا، ويبدأ كل تطبيق يُحمَّل لاحقًا بالتجاوزات في إطاره الأول. يعيد setThemeOverrides(nil) للتطبيق سمته الخاصة.

#الروابط التي يفتحها التطبيق

تصل عناوين URL التي يفتحها التطبيق (openUrl، وروابط TextView مع links="open") إلى onOpenURL أولًا، على الخيط الرئيسي: أعد true عندما يعالج التطبيق المضيف عنوان URL، كأن يمرره عبر الموجّه (router) الخاص به، وأعد false ليفتحه النظام باستخدام UIApplication.open. ودون onOpenURL يفتح النظام كل عناوين URL. وتصل عناوين URL إليه بعد أن تسمح بها صلاحية openUrl الخاصة بالتطبيق.

app.onOpenURL = { url in
    // The host app's own links open in the host app.
    if url.scheme == "lb" || url.host == "longbridge.com" {
        router.open(url)
        return true
    }
    return false
}

#الحدود

app.limits = NshLimits(memoryMB: 128, callBudget: 1, maxNodes: 50_000)
حقل NshLimitsالافتراضيالمعنى
memoryMB: UInt32?256كومة JS بوحدة MB. نفاد الذاكرة يوقف التطبيق.
stackKB: UInt32?4096 (وهو الحد الأقصى أيضًا)مكدس المفسّر بوحدة KB. التكرار الأعمق يرمي RangeError.
loadBudget: TimeInterval?10 sزمن تقييم الحزمة.
callBudget: TimeInterval?2 sزمن أي استدعاء آخر إلى JS (حدث واحد، أو مؤقت واحد، أو إطار واحد). يُقطع الاستدعاء المتجاوز.
maxNodes: UInt32?200 000عدد العُقد التي يجوز للتطبيق الاحتفاظ بها.

يحتفظ nil بالقيمة الافتراضية. وما يفعله كل حد عند بلوغه: العزل والصلاحيات.

#الصلاحيات

يصرّح التطبيق بما يمكنه الوصول إليه في nsh.json؛ ويمكن للتطبيق المضمِّن منح أقل من ذلك. يجب أن يسمح بالطلب كلاهما:

try app.grantCapabilities("""
{
  "network": { "hosts": ["api.example.com", "*.cdn.example.com"] },
  "webview": { "hosts": ["help.example.com"] }
}
""")
try app.loadBundle(at: url)

هنا لا يجوز للتطبيق ضبط شريط الحالة (لا يوجد statusBar)، حتى لو طلب nsh.json الخاص به ذلك. التفاصيل: العزل والصلاحيات.

#الحزم الموقّعة

لتشغيل كود ينزّله التطبيق (تحديثات، أو تطبيقات مصغّرة من جهات خارجية)، استخدم الحزم الموقّعة: يكتب nsh publish ملف .nshpkg موقّعًا بمفتاح إصدار صدّق عليه مفتاح الجذر الخاص بك، ويتحقق منه التطبيق قبل تشغيله.

let support = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
let trust = try NshTrust(stateDirectory: support.appendingPathComponent("nsh"))
try trust.addRoot(rootPublicKey)   // the contents of root.pub from `nsh keygen`

Task {
    do {
        let info = try await app.loadPackage(at: downloadedFile, trust: trust)
        print("running \(info.id) \(info.version) (build \(info.build))")
    } catch {
        print("refused: \(error)")   // the running app stays
    }
}

للتحقق أولًا والتشغيل لاحقًا، افصل الخطوتين:

let package = try await trust.openPackage(at: downloadedFile)   // verifies off the main thread
// ... decide, ask the user, ...
app.run(package)
APIالوصف
NshTrust(stateDirectory: URL) throwsيفتح مخزن الثقة أو ينشئه. تبقى حالته (أحدث قائمة إبطال، وأعلى بناء مقبول لكل تطبيق) فيه، وتُستخرج أصول الحزم تحته. لا يثق بأي مفتاح إلى أن يُستدعى addRoot. آمن للاستخدام عبر الخيوط.
addRoot(_ publicKey: String) throwsيثق بمفتاح جذر عام (base64، محتويات ملف .pub). يمكن الوثوق بعدة جذور.
updateRevocations(_ json: String) throwsيطبّق قائمة إبطال كتبها nsh revoke، وموقّعة من جذر موثوق، وأحدث من القائمة التي يحتفظ بها المخزن.
openPackage(at: URL) async throws -> NshPackageيتحقق، ويستخرج الأصول ويقبل الحزمة (تُرفض البنيات الأقدم من ذلك التطبيق منذ ذلك الحين).
NshPackage.infoid وname وversion وbuild للتطبيق الموجود داخلها.
NshErrorالخطأ الذي ترميه كل استدعاءات الرمي؛ ويوضح message السبب.

#التطوير

مع NativeShellDev.xcframework، وجّه العرض (view) إلى خادم التطوير بدلًا من الحزمة (bundle):

#if DEBUG
try app.connectDevServer(URL(string: "ws://127.0.0.1:8790/ws")!)
#else
try app.loadBundle(at: bundledURL)
#endif

شغّل nsh dev App.tsx على جهاز Mac الخاص بك. يشارك المحاكي شبكة جهاز Mac، لذلك يصل 127.0.0.1 إلى الخادم؛ أما الجهاز الفعلي فيستخدم عنوان الشبكة المحلية الذي يطبعه nsh dev (devices on this network: ws://192.168.1.20:8790/ws). يؤدي كل حفظ إلى تحديث التطبيق في مكانه، وتظهر السجلات في الطرفية nsh dev، وتظهر الأخطاء فوق التطبيق. راجع خادم التطوير وتصحيح الأخطاء.

#ملاحظات

  • الخيوط (Threads). NshAppView معزول على الخيط الرئيسي (main-actor). أما NshTrust وNshPackage وNshComponentContext فهي آمنة للاستخدام عبر الخيوط (thread-safe). يعمل محرك JS على خيط خاص به؛ ولا ينتظره الخيط الرئيسي أبدًا.
  • شريط الحالة (Status bar). تغيّر صفحات التطبيق المضمَّن وحواراته (statusBar) ما تعيده NshAppView.preferredStatusBarStyle وما شابهها؛ ويجب أن يمرّرها view controller المستضيف، كما في المثال أعلاه.
  • المنطقة الآمنة (Safe area). تستخدم التطبيقات safe_area() في Rust، وenv(safe-area-inset-*) في الحشو (padding) أو safeAreaInsets() من nsh في TSX، كالمعتاد. أما NshAppView الموضوع أسفل الترويسة الخاصة بك فيحصل على الهوامش التي تنطبق عليه، لا على هوامش النافذة.
  • Mac Catalyst. يعمل الإطار نفسه في تطبيقات Catalyst.

#انظر أيضًا