إنشاء تطبيق دردشة بدائي للغاية في SwiftUI، أثناء استخدام Swift وWebSockets لإنشاء خادم الدردشة. إنها سريعة من أعلى إلى أسفل، يا نحلة!
في هذا البرنامج التعليمي، سنقوم بإنشاء تطبيق دردشة بدائي ولكنه عملي. سيتم تشغيل التطبيق على نظام iOS أو macOS - أو كليهما! يكمن جمال SwiftUI في قلة الجهد المبذول لإنشاء تطبيق متعدد المنصات.
بالطبع، لن يكون لتطبيق الدردشة فائدة كبيرة بدون خادم للتحدث معه. ومن ثم، سنقوم أيضًا بإنشاء خادم دردشة بدائي جدًا، باستخدام WebSockets. سيتم إنشاء كل شيء بلغة Swift وتشغيله محليًا على جهازك.
يفترض هذا البرنامج التعليمي أن لديك بالفعل القليل من الخبرة في تطوير تطبيقات iOS/macOS باستخدام SwiftUI. على الرغم من أنه سيتم شرح المفاهيم مع تقدمنا، إلا أنه لن تتم تغطية كل شيء بعمق. وغني عن القول، إذا كتبت واتبعت الخطوات، بحلول نهاية هذا البرنامج التعليمي، سيكون لديك تطبيق دردشة فعال (لنظامي التشغيل iOS و/أو macOS)، والذي يتصل بخادم قمت بإنشائه أيضًا! سيكون لديك أيضًا فهم أساسي لمفاهيم مثل Swift وWebSockets من جانب الخادم.
إذا لم يكن أي من ذلك يثير اهتمامك، فيمكنك دائمًا التمرير حتى النهاية وتنزيل كود المصدر النهائي!
باختصار، سنبدأ بإنشاء خادم بسيط جدًا، وسهل، وخالي من الميزات. سنقوم ببناء الخادم كحزمة Swift، ثم نضيف إطار عمل Vapor على الويب باعتباره تبعية. سيساعدنا هذا في إعداد خادم WebSocket باستخدام بضعة أسطر فقط من التعليمات البرمجية.
بعد ذلك سنبدأ في إنشاء تطبيق الدردشة الأمامي. البدء سريعًا بالأساسيات، ثم إضافة الميزات (والضروريات) واحدة تلو الأخرى.
سنقضي معظم وقتنا في العمل على التطبيق، ولكننا سنتنقل ذهابًا وإيابًا بين رمز الخادم ورمز التطبيق عندما نضيف ميزات جديدة.
خياري
لنبدأ!
افتح Xcode 12 وابدأ مشروعًا جديدًا ( File > New Project ). ضمن Multiplaform، حدد Swift Package .
أطلق على الحزمة اسمًا منطقيًا - شيئًا لا يحتاج إلى شرح - مثل " ChatServer ". ثم احفظه أينما تريد.
باقة سويفت؟
عند إنشاء إطار عمل أو برنامج متعدد المنصات (مثل Linux) في Swift، فإن حزم Swift هي الطريقة المفضلة لذلك. إنها الحل الرسمي لإنشاء تعليمات برمجية معيارية يمكن لمشاريع Swift الأخرى استخدامها بسهولة. لا يجب بالضرورة أن تكون حزمة Swift مشروعًا معياريًا: يمكن أيضًا أن تكون ملفًا مستقلاً قابلاً للتنفيذ يستخدم ببساطة حزم Swift الأخرى كتبعيات (وهو ما نفعله).
ربما خطر لك أنه لا يوجد مشروع Xcode (
.xcodeproj
) موجود في حزمة Swift. لفتح حزمة Swift في Xcode مثل أي مشروع آخر، ما عليك سوى فتح الملفPackage.swift
. يجب أن يتعرف Xcode على أنك تفتح حزمة Swift ويفتح بنية المشروع بالكامل. سيتم تلقائيًا جلب جميع التبعيات في البداية.يمكنك قراءة المزيد عن حزم Swift ومدير حزم Swift على موقع Swift الرسمي.
للتعامل مع كل الأعباء الثقيلة لإعداد الخادم، سنستخدم إطار عمل الويب Vapor. يأتي Vapor مزودًا بجميع الميزات الضرورية لإنشاء خادم WebSocket.
ويب سوكيتس؟
لتزويد الويب بالقدرة على التواصل مع الخادم في الوقت الفعلي، تم إنشاء WebSockets. إنها مواصفات موصوفة جيدًا للاتصال الآمن في الوقت الفعلي (النطاق الترددي المنخفض) بين العميل والخادم. على سبيل المثال: الألعاب متعددة اللاعبين وتطبيقات الدردشة. تلك الألعاب متعددة اللاعبين التي تسبب الإدمان والتي كنت تلعبها في وقت الشركة الثمين؟ نعم، ويب سوكيتس!
ومع ذلك، إذا كنت ترغب في القيام بشيء مثل بث الفيديو في الوقت الفعلي، فمن الأفضل أن تبحث عن حل مختلف. ؟
على الرغم من أننا نقوم بإنشاء تطبيق دردشة iOS/macOS في هذا البرنامج التعليمي، إلا أن الخادم الذي نقوم بإنشائه يمكنه التحدث بسهولة مع الأنظمة الأساسية الأخرى باستخدام WebSockets. في الواقع: إذا أردت، يمكنك أيضًا إنشاء إصدار Android وإصدار الويب من تطبيق الدردشة هذا، والتحدث إلى نفس الخادم والسماح بالاتصال بين جميع الأنظمة الأساسية!
بخار؟
الإنترنت عبارة عن سلسلة معقدة من الأنابيب. حتى الاستجابة لطلب HTTP بسيط يتطلب قدرًا كبيرًا من التعليمات البرمجية. لحسن الحظ، قام الخبراء في هذا المجال بتطوير أطر ويب مفتوحة المصدر تقوم بكل العمل الشاق لنا منذ عقود، بلغات برمجة مختلفة. البخار هو واحد منهم، وهو مكتوب بلغة سويفت. إنه يأتي بالفعل مع بعض إمكانيات WebSocket وهو بالضبط ما نحتاجه.
على الرغم من ذلك، فإن Vapor ليس إطار عمل الويب الوحيد الذي يعمل بنظام Swift. Kitura و Perfect هما إطاران معروفان أيضًا. على الرغم من أن Vapor يمكن القول إنه أكثر نشاطًا في تطوره.
يجب أن يفتح Xcode ملف Package.swift
افتراضيًا. هذا هو المكان الذي نضع فيه المعلومات والمتطلبات العامة لحزمة Swift الخاصة بنا.
قبل أن نفعل ذلك، ابحث في مجلد Sources/ChatServer
. يجب أن يحتوي على ملف ChatServer.swift
. نحن بحاجة إلى إعادة تسمية هذا إلى main.swift
. بمجرد الانتهاء من ذلك، ارجع إلى Package.swift
.
ضمن products:
قم بإزالة القيمة التالية:
. library ( name : " ChatServer " , targets : [ " ChatServer " ] )
...واستبدله بـ:
. executable ( name : " ChatServer " , targets : [ " ChatServer " ] )
بعد كل شيء، خادمنا ليس مكتبة. ولكن قائمة بذاتها قابلة للتنفيذ، بدلا من ذلك. يجب علينا أيضًا تحديد الأنظمة الأساسية (والحد الأدنى من الإصدار) التي نتوقع أن يعمل عليها خادمنا. يمكن القيام بذلك عن طريق إضافة platforms: [.macOS(v10_15)]
تحت name: "ChatServer"
:
name: " ChatServer " ,
platforms: [
. macOS ( . v10_15 ) ,
] ,
كل هذا من شأنه أن يجعل حزمة Swift الخاصة بنا "قابلة للتشغيل" في Xcode.
حسنًا، دعنا نضيف Vapor باعتباره تبعية. في dependencies: []
(الذي يجب أن يحتوي على بعض العناصر التي تم التعليق عليها)، أضف ما يلي:
. package ( url : " https://github.com/vapor/vapor.git " , from : " 4.0.0 " )
عند حفظ ملف Package.swift
، يجب أن يبدأ Xcode تلقائيًا في جلب تبعيات Vapor مع الإصدار 4.0.0
أو الأحدث. وكذلك جميع تبعياتها .
علينا فقط إجراء تعديل آخر على الملف بينما يقوم Xcode بعمله: إضافة التبعية إلى هدفنا. في targets:
ستجد .target(name: "ChatServer", dependencies: [])
. في تلك المصفوفة الفارغة، أضف ما يلي:
. product ( name : " Vapor " , package : " vapor " )
هذا كل شيء . تم الانتهاء من Package.swift
الخاصة بنا. لقد وصفنا حزمة Swift الخاصة بنا بإخبارها:
يجب أن تبدو الحزمة النهائية Package.swift
بالشكل التالي(-ish):
// swift-tools-version:5.3
import PackageDescription
let package = Package (
name : " ChatServer " ,
platforms : [
. macOS ( . v10_15 ) ,
] ,
products : [
. executable ( name : " ChatServer " , targets : [ " ChatServer " ] )
] ,
dependencies : [
. package ( url : " https://github.com/vapor/vapor.git " , from : " 4.0.0 " )
] ,
targets : [
. target (
name : " ChatServer " ,
dependencies : [
. product ( name : " Vapor " , package : " vapor " )
] ) ,
]
)
الآن، أخيراً حان الوقت ل...
في Xcode، افتح Sources/ChatServer/main.swift
واحذف كل شيء هناك. انها لا قيمة لها بالنسبة لنا. بدلاً من ذلك، اجعل main.swift
يبدو كما يلي:
import Vapor
var env = try Environment . detect ( ) // 1
let app = Application ( env ) // 2
defer { // 3
app . shutdown ( )
}
app . webSocket ( " chat " ) { req , client in // 4
print ( " Connected: " , client )
}
try app . run ( ) // 5
؟ بام! هذا كل ما يتطلبه الأمر لبدء خادم (WebSocket) باستخدام Vapor. انظر إلى مدى سهولة ذلك.
defer
واتصل بـ .shutdown()
الذي سيقوم بأي عملية تنظيف عند الخروج من البرنامج./chat
. الآن
بمجرد تشغيل البرنامج بنجاح، قد لا ترى أي شيء يشبه التطبيق. وذلك لأن برامج الخادم لا تحتوي على واجهات مستخدم رسومية. لكن كن مطمئنًا، فالبرنامج على قيد الحياة وبصحة جيدة في الخلفية، ويدير عجلاته. ومع ذلك، يجب أن تعرض وحدة تحكم Xcode الرسالة التالية:
notice codes.vapor.application : Server starting on http://127.0.0.1:8080
وهذا يعني أن الخادم يمكنه الاستماع بنجاح للطلبات الواردة. وهذا أمر رائع، لأن لدينا الآن خادم WebSocket يمكننا البدء في الاتصال به!
أنا لا أصدقك؟
إذا كنت تعتقد لأي سبب من الأسباب أنني لم أقم بإلقاء أي شيء سوى الأكاذيب الشنيعة طوال الوقت، فيمكنك اختبار الخادم بنفسك!
افتح متصفحك المفضل وتأكد من أنك في علامة تبويب فارغة. (إذا كان Safari، فستحتاج إلى تمكين وضع المطور أولاً.) افتح المفتش (
Cmd
+Option
+I
) وانتقل إلى وحدة التحكم . اكتبnew WebSocket ( 'ws://localhost:8080/chat' )واضغط على "رجوع". ألقِ نظرة الآن على وحدة تحكم Xcode. إذا سارت الأمور على ما يرام، فمن المفترض أن يظهر الآن
Connected: WebSocketKit.WebSocket
.؟؟؟؟
لا يمكن الوصول إلى الخادم إلا من جهازك المحلي. هذا يعني أنه لا يمكنك توصيل جهاز iPhone/iPad الفعلي بالخادم. بدلاً من ذلك، سنستخدم المحاكي في الخطوات التالية لاختبار تطبيق الدردشة الخاص بنا.
لاختبار تطبيق الدردشة على جهاز فعلي، يجب اتخاذ بعض الخطوات الإضافية (الصغيرة). راجع الملحق أ.
على الرغم من أننا لم ننته بعد من الواجهة الخلفية، فقد حان الوقت للانتقال إلى الواجهة الأمامية. تطبيق الدردشة نفسه!
في Xcode قم بإنشاء مشروع جديد. هذه المرة، ضمن Multiplatform، حدد التطبيق . مرة أخرى، اختر اسمًا جميلاً لتطبيقك وتابع. (لقد اخترت SwiftChat . أوافق، فهو مثالي ؟)
لا يعتمد التطبيق على أي أطر عمل أو مكتبات خارجية. في الواقع، كل ما نحتاجه متاح عبر Foundation
و Combine
و SwiftUI
(في Xcode 12+).
لنبدأ العمل على شاشة الدردشة على الفور. قم بإنشاء ملف Swift جديد وقم بتسميته ChatScreen.swift
. لا يهم ما إذا كنت اخترت ملف Swift أو قالب عرض SwiftUI . نحن نقوم بحذف كل شيء فيه بغض النظر.
إليك مجموعة الأدوات الأولية لـ ChatScreen.swift
:
import SwiftUI
struct ChatScreen : View {
@ State private var message = " "
var body : some View {
VStack {
// Chat history.
ScrollView { // 1
// Coming soon!
}
// Message field.
HStack {
TextField ( " Message " , text : $message ) // 2
. padding ( 10 )
. background ( Color . secondary . opacity ( 0.2 ) )
. cornerRadius ( 5 )
Button ( action : { } ) { // 3
Image ( systemName : " arrowshape.turn.up.right " )
. font ( . system ( size : 20 ) )
}
. padding ( )
. disabled ( message . isEmpty ) // 4
}
. padding ( )
}
}
}
في ContentsView.swift
، استبدل Hello World بـ ChatScreen()
:
struct ContentView : View {
var body : some View {
ChatScreen ( )
}
}
قماش فارغ، في الوقت الراهن.
على اليسار: iPhone ذو مظهر داكن. على اليمين: iPad بمظهر خفيف.
ما لدينا هنا:
إذا كنت ترغب في اتخاذ خيارات تصميم مختلفة، فاستمر في ذلك. ؟
لنبدأ الآن العمل على بعض المنطق غير المتعلق بواجهة المستخدم: الاتصال بالخادم نفسه الذي أنشأناه للتو.
يوفر SwiftUI ، جنبًا إلى جنب مع إطار العمل Combine ، للمطورين أدوات لتنفيذ الفصل بين الاهتمامات بسهولة في التعليمات البرمجية الخاصة بهم. باستخدام بروتوكول ObservableObject
وأغلفة الخصائص @StateObject
(أو @ObservedObject
) يمكننا تنفيذ منطق غير واجهة المستخدم (يشار إليه باسم Business Logic ) في مكان منفصل. كما ينبغي أن تكون الأمور! بعد كل شيء، الشيء الوحيد الذي يجب أن تهتم به واجهة المستخدم هو عرض البيانات للمستخدم والتفاعل مع مدخلات المستخدم. لا ينبغي أن تهتم بمصدر البيانات أو كيفية معالجتها.
قادمة من خلفية React، هذا الترف هو شيء أحسده بشكل لا يصدق.
هناك الآلاف والآلاف من المقالات والمناقشات حول هندسة البرمجيات. من المحتمل أنك سمعت أو قرأت عن مفاهيم مثل MVC وMVVM وVAPOR وClean Architecture والمزيد. لديهم جميعا حججهم وتطبيقاتهم.
مناقشة هذه الأمور خارج نطاق هذا البرنامج التعليمي. ولكن من المتفق عليه عمومًا أن منطق الأعمال ومنطق واجهة المستخدم لا ينبغي أن يتشابكا.
ينطبق هذا المفهوم بنفس القدر على ChatScreen الخاص بنا. الشيء الوحيد الذي يجب أن يهتم به ChatScreen هو عرض الرسائل والتعامل مع النص الذي يدخله المستخدم. لا يهتم بـ ✌️We Bs Oc K eTs✌، ولا ينبغي له ذلك.
يمكنك إنشاء ملف Swift جديد أو كتابة الكود التالي أسفل ChatScreen.swift
. اختيارك. أينما يعيش، تأكد من عدم نسيان import
!
import Combine
import Foundation
final class ChatScreenModel : ObservableObject {
private var webSocketTask : URLSessionWebSocketTask ? // 1
// MARK: - Connection
func connect ( ) { // 2
let url = URL ( string : " ws://127.0.0.1:8080/chat " ) ! // 3
webSocketTask = URLSession . shared . webSocketTask ( with : url ) // 4
webSocketTask ? . receive ( completionHandler : onReceive ) // 5
webSocketTask ? . resume ( ) // 6
}
func disconnect ( ) { // 7
webSocketTask ? . cancel ( with : . normalClosure , reason : nil ) // 8
}
private func onReceive ( incoming : Result < URLSessionWebSocketTask . Message , Error > ) {
// Nothing yet...
}
deinit { // 9
disconnect ( )
}
}
قد يكون هذا كثيرًا لاستيعابه، لذلك دعونا نتناوله ببطء:
URLSessionWebSocketTask
في إحدى الممتلكات.URLSessionWebSocketTask
مسؤولة عن اتصالات WebSocket. إنهم مقيمون في عائلة URLSession
في إطار عمل المؤسسة .127.0.0.1
أو localhost
). المنفذ الافتراضي لتطبيقات Vapor هو 8080
. ونضع مستمعًا لاتصالات WebSocket في مسار /chat
.URLSessionWebSocketTask
وتخزينه في خاصية المثيل.onReceive(incoming:)
. المزيد عن هذا لاحقا.ChatScreenModel
من الذاكرة. هذه بداية عظيمة. لدينا الآن مكان يمكننا من خلاله وضع كل منطق WebSocket الخاص بنا دون تشويش كود واجهة المستخدم. لقد حان الوقت لتواصل ChatScreen
مع ChatScreenModel
.
أضف ChatScreenModel
ككائن حالة في ChatScreen
:
struct ChatScreen : View {
@ StateObject private var model = ChatScreenModel ( ) // <- this here
@ State private var message = " "
// etc...
}
متى يجب أن نتصل بالخادم؟ حسنًا، عندما تكون الشاشة مرئية بالفعل ، بالطبع. قد تميل إلى الاتصال بـ .connect()
في init()
الخاص بـ ChatScreen
. وهذا أمر خطير. في الواقع، في SwiftUI، يجب على المرء أن يحاول تجنب وضع أي شيء في init()
، حيث يمكن تهيئة العرض حتى عندما لا يظهر أبدًا. (على سبيل المثال في LazyVStack
أو في NavigationLink(destination:)
.) سيكون من العار إضاعة دورات وحدة المعالجة المركزية الثمينة. لذلك، دعونا نؤجل كل شيء إلى onAppear
.
أضف طريقة onAppear
إلى ChatScreen
. ثم قم بإضافة هذه الطريقة وتمريرها إلى مُعدِّل .onAppear(perform:)
الخاص بـ VStack
:
struct ChatScreen : View {
// ...
private func onAppear ( ) {
model . connect ( )
}
var body : some View {
VStack {
// ...
}
. onAppear ( perform : onAppear )
}
}
مساحة مهدرة؟
يفضل الكثير من الأشخاص كتابة محتويات هذه الطرق مضمّنة بدلاً من ذلك:
. onAppear { model . connect ( ) }وهذا ليس سوى تفضيل شخصي. أنا شخصياً أحب تحديد هذه الطرق بشكل منفصل. نعم، يكلف مساحة أكبر. لكن من السهل العثور عليها، وقابلة لإعادة الاستخدام، وتمنع
body
من التشوش (المزيد) ويمكن القول إنها أسهل في الطي. ؟
وعلى نفس المنوال، يجب علينا أيضًا قطع الاتصال عندما يختفي العرض. يجب أن يكون التنفيذ واضحًا بذاته، ولكن فقط في حالة:
struct ChatScreen : View {
// ...
private func onDisappear ( ) {
model . disconnect ( )
}
var body : some View {
VStack {
// ...
}
. onAppear ( perform : onAppear )
. onDisappear ( perform : onDisappear )
}
}
من المهم جدًا إغلاق اتصالات WebSocket عندما نتوقف عن الاهتمام بها. عندما تقوم (برشاقة) بإغلاق اتصال WebSocket، سيتم إعلام الخادم ويمكنه مسح الاتصال من الذاكرة. يجب ألا يحتوي الخادم مطلقًا على اتصالات ميتة أو غير معروفة باقية في الذاكرة.
أوف. إنها رحلة مررنا بها حتى الآن. حان الوقت لاختباره.ChatScreen
، يجب أن تشاهد رسالة Connected: WebSocketKit.WebSocket
في وحدة تحكم Xcode الخاصة بالخادم. إذا لم يكن الأمر كذلك، قم بتتبع خطواتك وابدأ في تصحيح الأخطاء!
شيء آخر™️. يجب علينا أيضًا اختبار ما إذا كان اتصال WebSocket مغلقًا عندما يغلق المستخدم التطبيق (أو يغادر ChatScreen
). عد إلى الملف main.swift
الخاص بمشروع الخادم. يبدو مستمع WebSocket حاليًا كما يلي:
app . webSocket ( " chat " ) { req , client in
print ( " Connected: " , client )
}
قم بإضافة معالج إلى .onClose
الخاص client
، دون إجراء أي شيء سوى عملية print()
بسيطة:
app . webSocket ( " chat " ) { req , client in
print ( " Connected: " , client )
client . onClose . whenComplete { _ in
print ( " Disconnected: " , client )
}
}
أعد تشغيل الخادم وابدأ تطبيق الدردشة. بمجرد توصيل التطبيق، أغلق التطبيق (اخرج منه فعليًا، ولا تضعه في الخلفية فقط). يجب الآن على وحدة تحكم Xcode الخاصة بالخادم طباعة Disconnected: WebSocketKit.WebSocket
. وهذا يؤكد أن اتصالات WebSocket مغلقة بالفعل عندما لم نعد نهتم بها. وبالتالي، يجب ألا يكون لدى الخادم أي اتصالات ميتة باقية في الذاكرة.
هل أنت مستعد لإرسال شيء ما إلى الخادم؟ الصبي، أنا متأكد من ذلك. لكن للحظة فقط، دعونا نتوقف ونفكر للحظة. استند إلى ظهر الكرسي وحدق بلا هدف، ولكن بشكل هادف إلى السقف...
ما الذي سنرسله بالضبط إلى الخادم؟ وبنفس القدر من الأهمية، ما الذي سنتلقاه من الخادم؟
قد تكون فكرتك الأولى هي "حسنًا، فقط أرسل رسالة نصية، أليس كذلك؟"، ستكون على حق. ولكن ماذا عن وقت الرسالة؟ وماذا عن اسم المرسل؟ ماذا عن المعرف الذي يجعل الرسالة فريدة من نوعها عن أي رسالة أخرى؟ ليس لدينا أي شيء للمستخدم لإنشاء اسم مستخدم أو أي شيء حتى الآن. لذلك دعونا نضع ذلك جانبًا ونركز فقط على إرسال واستقبال الرسائل.
سيتعين علينا إجراء بعض التعديلات على جانب التطبيق والخادم. لنبدأ بالخادم.
قم بإنشاء ملف Swift جديد في Sources/ChatServer
يسمى Models.swift
في مشروع الخادم. الصق (أو اكتب) الكود التالي في Models.swift
:
import Foundation
struct SubmittedChatMessage : Decodable { // 1
let message : String
}
struct ReceivingChatMessage : Encodable , Identifiable { // 2
let date = Date ( ) // 3
let id = UUID ( ) // 4
let message : String // 5
}
إليك ما يحدث:
Decodable
.Encodable
.ReceivingChatMessage
. لاحظ كيف نقوم بإنشاء date
id
على جانب الخادم. وهذا يجعل الخادم مصدر الحقيقة. الخادم يعرف ما هو الوقت. إذا تم إنشاء التاريخ من جانب العميل، فلا يمكن الوثوق به. ماذا لو كان لدى العميل إعداد ساعته ليكون في المستقبل؟ إن قيام الخادم بإنشاء التاريخ يجعل ساعته هي المرجع الوحيد للوقت.
المناطق الزمنية؟
يحتوي كائن
Date
Swift دائمًا على 00:00:00 UTC 01-01-2001 كوقت مرجعي مطلق. عند تهيئةDate
أو تنسيق واحد إلى سلسلة (على سبيل المثال عبرDateFormatter
)، سيتم أخذ منطقة العميل في الاعتبار تلقائيًا. إضافة أو تخفيض الساعات حسب المنطقة الزمنية للعميل.
UUID؟
على المستوى العالمي، تعتبر معرفات الهوية الفريدة عالميًا بمثابة قيم مقبولة للمعرفات.
لا نريد أيضًا أن يرسل العميل رسائل متعددة بنفس المعرف الفريد. سواء كان ذلك عن طريق الخطأ أو عن قصد. يعد إنشاء الخادم لهذا المعرف بمثابة طبقة إضافية من الأمان وتقليل مصادر الأخطاء المحتملة.
الآن بعد ذلك. عندما يتلقى الخادم رسالة من عميل، يجب عليه تمريرها إلى كل عميل آخر. ومع ذلك، فإن هذا يعني أنه يتعين علينا تتبع كل عميل متصل.
العودة إلى main.swift
لمشروع الخادم. مباشرة فوق app.webSocket("chat")
ضع الإعلان التالي:
var clientConnections = Set < WebSocket > ( )
هذا هو المكان الذي سنقوم فيه بتخزين اتصالات عملائنا.
لكن مهلا ... من المفترض أن تحصل على خطأ برمجي كبير وسيئ وسيئ. وذلك لأن كائن WebSocket
لا يتوافق مع بروتوكول Hashable
افتراضيًا. ومع ذلك، لا تقلق، يمكن تنفيذ ذلك بسهولة (ولو بتكلفة زهيدة). أضف الكود التالي في أسفل main.swift
:
extension WebSocket : Hashable {
public static func == ( lhs : WebSocket , rhs : WebSocket ) -> Bool {
ObjectIdentifier ( lhs ) == ObjectIdentifier ( rhs )
}
public func hash ( into hasher : inout Hasher ) {
hasher . combine ( ObjectIdentifier ( self ) )
}
}
بادابينج بادابوم. الكود أعلاه هو طريقة سريعة ولكن بسيطة لجعل class
يتوافق مع Hashable
(وبالتعريف أيضًا Equatable
)، وذلك ببساطة عن طريق استخدام عنوان الذاكرة الخاص به كخاصية فريدة. ملاحظة : هذا يعمل فقط للفصول الدراسية. سوف تتطلب الهياكل المزيد من التنفيذ العملي.
حسنًا، الآن بعد أن أصبحنا قادرين على تتبع العملاء، استبدل كل شيء في app.webSocket("chat")
(بما في ذلك إغلاقه ومحتوياته ) بالرمز التالي ?:
app . webSocket ( " chat " ) { req , client in
clientConnections . insert ( client )
client . onClose . whenComplete { _ in
clientConnections . remove ( client )
}
}
عندما يتصل العميل، قم بتخزين العميل المذكور في clientConnections
. عند قطع اتصال العميل، قم بإزالته من نفس Set
. عزبز.
الخطوة الأخيرة في هذا الفصل هي إضافة قلب الخادمclient.onClose.whenComplete
بالكامل - ولكن لا يزال داخل إغلاق app.webSocket("chat")
- أضف مقتطف التعليمات البرمجية التالي:
client . onText { _ , text in // 1
do {
guard let data = text . data ( using : . utf8 ) else {
return
}
let incomingMessage = try JSONDecoder ( ) . decode ( SubmittedChatMessage . self , from : data ) // 2
let outgoingMessage = ReceivingChatMessage ( message : incomingMessage . message ) // 3
let json = try JSONEncoder ( ) . encode ( outgoingMessage ) // 4
guard let jsonString = String ( data : json , encoding : . utf8 ) else {
return
}
for connection in clientConnections {
connection . send ( jsonString ) // 5
}
}
catch {
print ( error ) // 6
}
}
مرة أخرى من الأعلى:
.onText
بالعميل المتصل.ReceivingChatMessage
بالرسالة المستلمة من العميل.ReceivingChatMessage
تلقائيًا.ReceivingChatMessage
إلى سلسلة JSON (مثل Data
).لماذا إعادته؟
يمكننا استخدام هذا كتأكيد على أن الرسالة قد تم استلامها بنجاح من العميل. سيستقبل التطبيق الرسالة تمامًا مثلما يتلقى أي رسالة أخرى. سيمنعنا هذا من الاضطرار إلى كتابة تعليمات برمجية إضافية لاحقًا.
منتهي! الخادم جاهز لتلقي الرسائل وتمريرها إلى العملاء الآخرين المتصلين. قم بتشغيل الخادم واتركه خاملاً في الخلفية، بينما نواصل التطبيق!
هل تتذكر بنيات SubmittedChatMessage
و ReceivingChatMessage
التي أنشأناها للخادم؟ نحن في حاجة إليها للتطبيق كذلك. قم بإنشاء ملف Swift جديد وقم بتسميته Models.swift
. على الرغم من أنه يمكنك فقط نسخ ولصق التطبيقات، إلا أنها ستتطلب بعض التعديل:
import Foundation
struct SubmittedChatMessage : Encodable {
let message : String
}
struct ReceivingChatMessage : Decodable , Identifiable {
let date : Date
let id : UUID
let message : String
}
لاحظ كيف تم تبديل البروتوكولات Encodable
والقابلة Decodable
. هذا أمر منطقي: في التطبيق، نقوم فقط بتشفير SubmittedChatMessage
وفك تشفير ReceivingChatMessage
فقط. عكس الخادم . لقد قمنا أيضًا بإزالة التهيئة التلقائية date
id
. التطبيق ليس لديه عمل في توليد هذه.
حسنًا، عد إلى ChatScreenModel
(سواء كان ذلك في ملف منفصل أو في أسفل ChatScreen.swift
). أضف الجزء العلوي، ولكن داخل ChatScreenModel
قم بإضافة خاصية المثيل التالية:
@ Published private ( set ) var messages : [ ReceivingChatMessage ] = [ ]
هذا هو المكان الذي سنقوم فيه بتخزين الرسائل المستلمة. بفضل @Published
، ستعرف ChatScreen
بالضبط متى يتم تحديث هذه المصفوفة وسوف تتفاعل مع هذا التغيير. private(set)
يتأكد من أن ChatScreenModel
فقط يمكنه تحديث هذه الخاصية. (في نهاية المطاف، هو مالك البيانات. ولا يحق لأي كائن آخر تعديلها مباشرة!)
لا يزال داخل ChatScreenModel
، أضف الطريقة التالية:
func send ( text : String ) {
let message = SubmittedChatMessage ( message : text ) // 1
guard let json = try ? JSONEncoder ( ) . encode ( message ) , // 2
let jsonString = String ( data : json , encoding : . utf8 )
else {
return
}
webSocketTask ? . send ( . string ( jsonString ) ) { error in // 3
if let error = error {
print ( " Error sending message " , error ) // 4
}
}
}
يبدو واضحا. ولكن من أجل الاتساق:
SubmittedChatMessage
، في الوقت الحالي، تحتوي على الرسالة فقط. افتح ChatScreen.swift
وأضف الطريقة التالية إلى ChatScreen
:
private func onCommit ( ) {
if !message . isEmpty {
model . send ( text : message )
message = " "
}
}
سيتم استدعاء هذه الطريقة عندما يضغط المستخدم على زر الإرسال أو عند الضغط على زر الرجوع على لوحة المفاتيح. على الرغم من أنه لن يرسل الرسالة إلا إذا كانت تحتوي بالفعل على أي شيء .
في .body
ChatScreen
.، حدد موقع TextField
و Button
، ثم استبدلهما (ولكن ليس معدّلاتهم أو محتوياتهم) بالتهيئة التالية:
TextField ( " Message " , text : $message , onEditingChanged : { _ in } , onCommit : onCommit )
// .modifiers here
Button ( action : onCommit ) {
// Image etc
}
// .modifiers here
عند الضغط على مفتاح Return أثناء التركيز على TextField
، سيتم استدعاء onCommit
. وينطبق الشيء نفسه على Button
الذي يضغط عليه المستخدم. يتطلب TextField
أيضًا وسيطة onEditingChanged
- ولكننا نتجاهل ذلك من خلال إعطائها إغلاقًا فارغًا.
الآن هو الوقت المناسب للبدء في اختبار ما لدينا. تأكد من أن الخادم لا يزال يعمل في الخلفية. ضع بعض نقاط التوقف في إغلاق client.onText
(حيث يقرأ الخادم الرسائل الواردة) في main.swift
للخادم. قم بتشغيل التطبيق وأرسل رسالة. يجب الضغط على نقطة (نقاط) التوقف في main.swift
عند تلقي رسالة من التطبيق. إذا فعلت،؟ الخصبة ! ؟ إذا لم يكن الأمر كذلك، حسنًا... قم بتتبع خطواتك وابدأ في تصحيح الأخطاء!
إرسال الرسائل لطيف وكل شيء. ولكن ماذا عن استقبالهم؟ (حسنًا، من الناحية الفنية، نحن نستقبلهم، ولكننا لا نتفاعل معهم مطلقًا). أنت على حق!
لنقم بزيارة ChatScreenModel
مرة أخرى. تذكر أن طريقة onReceive(incoming:)
؟ استبدله واعطيه طريقة الأخوة كما هو موضح أدناه:
private func onReceive ( incoming : Result < URLSessionWebSocketTask . Message , Error > ) {
webSocketTask ? . receive ( completionHandler : onReceive ) // 1
if case . success ( let message ) = incoming { // 2
onMessage ( message : message )
}
else if case . failure ( let error ) = incoming { // 3
print ( " Error " , error )
}
}
private func onMessage ( message : URLSessionWebSocketTask . Message ) { // 4
if case . string ( let text ) = message { // 5
guard let data = text . data ( using : . utf8 ) ,
let chatMessage = try ? JSONDecoder ( ) . decode ( ReceivingChatMessage . self , from : data )
else {
return
}
DispatchQueue . main . async { // 6
self . messages . append ( chatMessage )
}
}
}
لذا...
URLSessionWebSocketTask
؟ إنهم يعملون مرة واحدة فقط. وبالتالي، فإننا نعيد ربط معالج جديد على الفور، حتى نكون مستعدين لقراءة الرسالة الواردة التالية.ReceivingChatMessage
.self.messages
. ومع ذلك ، نظرًا لأن URLSessionWebSocketTask
يمكنه استدعاء معالج الاستلام على سلسلة رسائل مختلفة، ولأن SwiftUI يعمل فقط على السلسلة الرئيسية، فيجب علينا تغليف تعديلنا في DispatchQueue.main.async {}
، مع التأكد من أننا نجري التعديل فعليًا على الخيط الرئيسي.إن شرح كيفية وأسباب العمل مع سلاسل الرسائل المختلفة في SwiftUI هو خارج نطاق هذا البرنامج التعليمي.
تقريبا هناك!
تحقق مرة أخرى على ChatScreen.swift
. هل ترى ذلك ScrollView
الفارغ؟ يمكننا أخيرًا ملؤها بالرسائل:
ScrollView {
LazyVStack ( spacing : 8 ) {
ForEach ( model . messages ) { message in
Text ( message . message )
}
}
}
لن يبدو الأمر مذهلاً بأي حال من الأحوال. ولكن هذا سوف يقوم بالمهمة في الوقت الحالي. نحن ببساطة نمثل كل رسالة Text
عادي.
المضي قدما، قم بتشغيل التطبيق. عند إرسال رسالة، يجب أن تظهر على الفور على الشاشة. يؤكد هذا أنه تم إرسال الرسالة بنجاح إلى الخادم، وأن الخادم أرسلها مرة أخرى إلى التطبيق بنجاح! الآن، إذا استطعت، افتح مثيلات متعددة للتطبيق (نصيحة: استخدم محاكيات مختلفة). لا يوجد حد فعليًا لعدد العملاء! استمتع بحفلة دردشة كبيرة لطيفة بمفردك.
استمر في إرسال الرسائل حتى لا يتبقى أي مكان على الشاشة. لاحظت أي شيء؟ اليرب. لا يتم تمرير ScrollView
تلقائيًا إلى الأسفل بمجرد تجاوز الرسائل الجديدة حدود الشاشة. ؟
يدخل...
تذكر أن الخادم يقوم بإنشاء معرف فريد لكل رسالة. يمكننا أخيرًا استخدامه بشكل جيد! كان الانتظار يستحق كل هذا العناء للحصول على هذه المكافأة المذهلة، أؤكد لك.
في ChatScreen
، قم بتحويل ScrollView
إلى هذا الجمال:
ScrollView {
ScrollViewReader { proxy in // 1
LazyVStack ( spacing : 8 ) {
ForEach ( model . messages ) { message in
Text ( message . message )
. id ( message . id ) // 2
}
}
. onChange ( of : model . messages . count ) { _ in // 3
scrollToLastMessage ( proxy : proxy )
}
}
}
ثم أضف الطريقة التالية:
private func scrollToLastMessage ( proxy : ScrollViewProxy ) {
if let lastMessage = model . messages . last { // 4
withAnimation ( . easeOut ( duration : 0.4 ) ) {
proxy . scrollTo ( lastMessage . id , anchor : . bottom ) // 5
}
}
}
ScrollView
في ScrollViewReader
.ScrollViewReader
proxy
الذي سنحتاجه قريبًا جدًا.model.messages.count
. عندما تتغير هذه القيمة، نسمي الطريقة التي أضفناها للتو، ونمررها proxy
الذي يوفره ScrollViewReader
..scrollTo(_:anchor:)
الخاصة بـ ScrollViewProxy
. هذا يخبر ScrollView
بالتمرير إلى العرض باستخدام المعرف المحدد. نحن نلف هذا بـ withAnimation {}
لتحريك التمرير.وهاهو الأمر...
هذه الرسائل غنية جدًا... ولكنها ستكون أكثر روعة إذا عرفنا من أرسل الرسائل وميزنا بصريًا بين الرسائل المستلمة والمرسلة.
وسنرفق أيضًا مع كل رسالة اسم مستخدم ومعرف مستخدم. نظرًا لأن اسم المستخدم لا يكفي لتحديد هوية المستخدم، فنحن بحاجة إلى شيء فريد. ماذا لو كان اسم المستخدم والجميع هو باتريك؟ سنواجه أزمة هوية ولن نتمكن من التمييز بين الرسائل التي يرسلها باتريك والرسائل التي يتلقاها باتريك .
وكما جرت العادة، نبدأ بالخادم، فهو أقل قدر من العمل.
افتح Models.swift
حيث حددنا كلاً من SubmittedChatMessage
و ReceivingChatMessage
. امنح كلاً من هؤلاء الأشرار user: String
و userID: UUID
، كما يلي:
struct SubmittedChatMessage : Decodable {
let message : String
let user : String // <- We
let userID : UUID // <- are
}
struct ReceivingChatMessage : Encodable , Identifiable {
let date = Date ( )
let id = UUID ( )
let message : String
let user : String // <- new
let userID : UUID // <- here
}
(لا تنسَ تحديث ملف Models.swift في مشروع التطبيق أيضًا!)
بالعودة إلى main.swift
، حيث من المفترض أن يتم الترحيب بك بخطأ، قم بتغيير تهيئة ReceivingChatMessage
إلى ما يلي:
let outgoingMessage = ReceivingChatMessage (
message : incomingMessage . message ,
user : incomingMessage . user ,
userID : incomingMessage . userID
)
وهذا كل شيء ! لقد انتهينا من الخادم. إنه مجرد التطبيق من هنا فصاعدا. امتداد المنزل!
في مشروع Xcode الخاص بالتطبيق، قم بإنشاء ملف Swift جديد يسمى UserInfo.swift
. ضع الكود التالي هناك:
import Combine
import Foundation
class UserInfo : ObservableObject {
let userID = UUID ( )
@ Published var username = " "
}
سيكون هذا هو EnvironmentObject
الخاص بنا حيث يمكننا تخزين اسم المستخدم الخاص بنا. وكما هو الحال دائمًا، فإن المعرف الفريد هو UUID غير قابل للتغيير يتم إنشاؤه تلقائيًا. من أين يأتي اسم المستخدم؟ سيقوم المستخدم بإدخال هذا عند فتح التطبيق، قبل أن تظهر له شاشة الدردشة.
وقت الملف الجديد: SettingsScreen.swift
. سيحتوي هذا الملف على نموذج الإعدادات البسيطة:
import SwiftUI
struct SettingsScreen : View {
@ EnvironmentObject private var userInfo : UserInfo // 1
private var isUsernameValid : Bool {
!userInfo . username . trimmingCharacters ( in : . whitespaces ) . isEmpty // 2
}
var body : some View {
Form {
Section ( header : Text ( " Username " ) ) {
TextField ( " E.g. John Applesheed " , text : $userInfo . username ) // 3
NavigationLink ( " Continue " , destination : ChatScreen ( ) ) // 4
. disabled ( !isUsernameValid )
}
}
. navigationTitle ( " Settings " )
}
}
UserInfo
التي تم إنشاؤها مسبقًا هنا باعتبارها EnvironmentObject
.TextField
بكتابة محتوياته مباشرة في userInfo.username
.NavigationLink
الذي سيعرض ChatScreen
عند الضغط عليه. يتم تعطيل الزر عندما يكون اسم المستخدم غير صالح. (هل لاحظت كيف قمنا بتهيئة ChatScreen
في NavigationLink
؟ لو أننا جعلنا ChatScreen
يتصل بالخادم في init()
الخاص به، لكان قد تم ذلك الآن !)إذا كنت ترغب في ذلك، يمكنك إضافة القليل من المهارة إلى الشاشة.
نظرًا لأننا نستخدم ميزات التنقل في SwiftUI، فنحن بحاجة إلى البدء باستخدام NavigationView
في مكان ما. ContentView
هو المكان المثالي لذلك. قم بتغيير تطبيق ContentView
كما يلي:
struct ContentView : View {
@ StateObject private var userInfo = UserInfo ( ) // 1
var body : some View {
NavigationView {
SettingsScreen ( )
}
. environmentObject ( userInfo ) // 2
. navigationViewStyle ( StackNavigationViewStyle ( ) ) // 3
}
}
UserInfo
و...EnvironmentObject
، مما يجعله في متناول جميع طرق العرض اللاحقة. الآن لإرسال بيانات UserInfo
مع الرسائل التي نرسلها إلى الخادم. انتقل إلى ChatScreenModel
(أينما وضعته). في الجزء العلوي من الفصل أضف الخصائص التالية:
final class ChatScreenModel : ObservableObject {
private var username : String ?
private var userID : UUID ?
// the rest ...
}
يجب أن يتلقى ChatModelScreen
هذه القيم عند الاتصال. ليست مهمة ChatModelScreen
معرفة مصدر هذه المعلومات. إذا قررنا في المستقبل تغيير مكان تخزين username
userID
، فيمكننا ترك ChatModelScreen
دون تغيير.
قم بتغيير طريقة connect()
لقبول هذه الخصائص الجديدة كوسائط:
func connect ( username : String , userID : UUID ) {
self . username = username
self . userID = userID
// etc ...
}
أخيرًا، في send(text:)
، نحتاج إلى تطبيق هذه القيم الجديدة على SubmittedChatMessage
الذي نرسله إلى الخادم:
func send ( text : String ) {
guard let username = username , let userID = userID else { // Safety first!
return
}
let message = SubmittedChatMessage ( message : text , user : username , userID : userID )
// Everything else ...
}
Aaaand هذا كل شيء بالنسبة لـ ChatScreenModel
. لقد انتهى . ؟؟
للمرة الأخيرة، افتح ChatScreen.swift
. في الجزء العلوي من ChatScreen
أضف:
@ EnvironmentObject private var userInfo : UserInfo
لا تنس تقديم username
ومعرف userID
إلى ChatScreenModel
عند ظهور العرض:
private func onAppear ( ) {
model . connect ( username : userInfo . username , userID : userInfo . userID )
}
الآن، مرة أخرى، كما جرت العادة: استند إلى ظهر هذا الكرسي وانظر إلى السقف. كيف ينبغي أن تبدو الرسائل النصية؟ إذا لم تكن في حالة مزاجية تسمح بالتفكير الإبداعي، فيمكنك استخدام طريقة العرض التالية التي تمثل رسالة واحدة مستلمة (ومرسلة):
struct ChatMessageRow : View {
static private let dateFormatter : DateFormatter = {
let formatter = DateFormatter ( )
formatter . dateStyle = . none
formatter . timeStyle = . short
return formatter
} ( )
let message : ReceivingChatMessage
let isUser : Bool
var body : some View {
HStack {
if isUser {
Spacer ( )
}
VStack ( alignment : . leading , spacing : 6 ) {
HStack {
Text ( message . user )
. fontWeight ( . bold )
. font ( . system ( size : 12 ) )
Text ( Self . dateFormatter . string ( from : message . date ) )
. font ( . system ( size : 10 ) )
. opacity ( 0.7 )
}
Text ( message . message )
}
. foregroundColor ( isUser ? . white : . black )
. padding ( 10 )
. background ( isUser ? Color . blue : Color ( white : 0.95 ) )
. cornerRadius ( 5 )
if !isUser {
Spacer ( )
}
}
}
}
إنها ليست مثيرة بشكل خاص. إليك ما يبدو عليه على iPhone:
(تذكر كيف يرسل الخادم أيضًا تاريخ الرسالة؟ يُستخدم هنا لعرض الوقت.)
تعتمد الألوان والموضع على خاصية isUser
التي تم تمريرها بواسطة الأصل. في هذه الحالة، هذا الوالد ليس سوى ChatScreen
. نظرًا لأن ChatScreen
لديه حق الوصول إلى الرسائل بالإضافة إلى UserInfo
، فهو المكان الذي يتم فيه وضع المنطق لتحديد ما إذا كانت الرسالة تخص المستخدم أم لا.
يستبدل ChatMessageRow
Text
الممل الذي استخدمناه من قبل لتمثيل الرسائل:
ScrollView {
ScrollViewReader { proxy in
LazyVStack ( spacing : 8 ) {
ForEach ( model . messages ) { message in
// This one right here ?, officer.
ChatMessageRow ( message : message , isUser : message . userID == userInfo . userID )
. id ( message . id )
}
}
// etc.
}
}
مرحبا بكم في خط النهاية! لقد قطعت كل الطريق هنا! للمرة الأخيرة،
من المفترض أن يكون لديك الآن تطبيق دردشة بدائي، لكن فعال. وكذلك خادم يتعامل مع الرسائل الواردة والصادرة. كل شيء مكتوب في سويفت!
تهاني! وشكرا جزيلا لك على القراءة! ؟
يمكنك تنزيل الكود النهائي من جيثب.
على اليسار: جهاز iPad صغير الحجم ذو مظهر داكن. على اليمين: هاتف iPhone ضخم ذو مظهر خفيف.
دعونا نلخص رحلتنا:
كل ذلك مع البقاء بالكامل داخل النظام البيئي السريع. لا توجد لغات برمجة إضافية ، لا كوكوابودس أو أي شيء.
بالطبع ، ما أنشأناه هنا ليس سوى جزء صغير من جزء من تطبيق و Server جاهز للإنتاج. لقد قطعنا الكثير من الزوايا لتوفير في الوقت المحدد والتعقيد. وغني عن القول أنه ينبغي أن يعطي فهمًا أساسيًا جدًا لكيفية عمل تطبيق الدردشة.
النظر في الميزات التالية ، ربما ، تنفذ نفسك:
ForEach
، نكرر كل رسالة في الذاكرة. تتبع برنامج الدردشة الحديثة فقط حفنة من الرسائل لتقديمها ، وتحميلها فقط في الرسائل القديمة بمجرد تمرير المستخدم.هذا urlsessionwebsockettask API
إذا كنت قد عملت مع WebSockets من قبل ، فيمكنك مشاركة الرأي القائل بأن واجهة برمجة تطبيقات Apple لـ WebSocket هي ... غير تقليدية. أنت بالتأكيد لست وحدك في هذا. إن الاضطرار إلى إعادة ربط معالج الاستلام باستمرار أمر غريب . إذا كنت تعتقد أنك أكثر راحة باستخدام واجهة برمجة تطبيقات WebSocket أكثر تقليدية لنظام التشغيل iOS و MacOS ، فإنني بالتأكيد أوصي StarsCream. لقد تم اختباره جيدًا وأداء ويعمل على الإصدارات القديمة من iOS.
حشرات الأخطاء
تمت كتابة هذا البرنامج التعليمي باستخدام Xcode 12 Beta 5 و IOS 14 Beta 5. تظهر الأخطاء وتختفي بين كل إصدار تجريبي جديد. من المستحيل للأسف التنبؤ بما سوف وما الذي لن يعمل في الإصدارات المستقبلية (بيتا).
لا يعمل الخادم على جهازك المحلي فحسب ، بل يمكن الوصول إليه فقط من جهازك المحلي. هذه ليست مشكلة عند تشغيل التطبيق في Simulator (أو كتطبيق MacOS على نفس الجهاز). ولكن تشغيل التطبيق على جهاز فعلي ، أو على جهاز Mac مختلف ، سيتعين الوصول إلى الخادم في شبكتك المحلية.
للقيام بذلك ، في main.swift
من رمز الخادم ، أضف السطر التالي مباشرة بعد تهيئة مثيل Application
:
app . http . server . configuration . hostname = " 0.0.0.0 "
الآن في ChatScreenModel
، في connect(username:userID:)
الطريقة ، تحتاج إلى تغيير عنوان URL لمطابقة IP المحلي لجهازك:
let url = URL ( string : " ws://127.0.0.1:8080/chat " ) !
//^^this^^^
يمكن العثور على IP المحلي في جهازك بطرق مختلفة. أنا شخصياً أقوم دائمًا بفتح تفضيلات النظام> شبكة ، حيث يتم عرض IP مباشرة ويمكن تحديده ونسخه.
تجدر الإشارة إلى أن معدل نجاح هذا يختلف بين الشبكات. هناك الكثير من العوامل (مثل الأمن) التي يمكن أن تمنع هذا من العمل.
شكرا جزيلا لك على القراءة! إذا كان لديك أي آراء حول هذه المقالة ، أو أفكار للتحسينات ، أو وجدت بعض الأخطاء ، من فضلك ، من فضلك ، يرجى إعلامي! سأبذل قصارى جهدي لتحسين هذا البرنامج التعليمي باستمرار. ؟