كيفية الاقتراب من أغلفة خصائص Swift

نشرت: 2022-03-11

بعبارات بسيطة ، غلاف الخاصية هو بنية عامة تغلف القراءة والكتابة للخاصية وتضيف سلوكًا إضافيًا إليها. نستخدمه إذا احتجنا إلى تقييد قيم الخصائص المتاحة ، أو إضافة منطق إضافي إلى الوصول للقراءة / الكتابة (مثل استخدام قواعد البيانات أو الإعدادات الافتراضية للمستخدم) ، أو إضافة بعض الطرق الإضافية.

أغلفة الممتلكات في Swift 5.1

تتناول هذه المقالة طريقة Swift 5.1 الجديدة لخصائص التغليف ، والتي تقدم بنية جديدة وأنظف.

النهج القديم

تخيل أنك تقوم بتطوير تطبيق ، ولديك كائن يحتوي على بيانات ملف تعريف المستخدم.

 struct Account { var firstName: String var lastName: String var email: String? } let account = Account(firstName: "Test", lastName: "Test", email: "[email protected]") account.email = "[email protected]" print(account.email)

تريد إضافة التحقق من البريد الإلكتروني — إذا كان عنوان البريد الإلكتروني للمستخدم غير صالح ، فيجب أن تكون خاصية email nil . ستكون هذه حالة جيدة لاستخدام غلاف خاصية لتغليف هذا المنطق.

 struct Email<Value: StringProtocol> { private var _value: Value? init(initialValue value: Value?) { _value = value } var value: Value? { get { return validate(email: _value) ? _value : nil } set { _value = newValue } } private func validate(email: Value?) -> Bool { guard let email = email else { return false } let regex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-za-z]{2,64}" let pred = NSPredicate(format: "SELF MATCHES %@", regex) return pred.evaluate(with: email) } }

يمكننا استخدام هذا الغلاف في بنية الحساب:

 struct Account { var firstName: String var lastName: String var email: Email<String> }

الآن ، نحن على يقين من أن خاصية البريد الإلكتروني يمكن أن تحتوي فقط على عنوان بريد إلكتروني صالح.

كل شيء يبدو على ما يرام ، باستثناء بناء الجملة.

 let account = Account(firstName: "Test", lastName: "Test", email: Email(initialValue: "[email protected]")) account.email.value = "[email protected]" print(account.email.value)

باستخدام غلاف الخاصية ، يصبح بناء الجملة الخاص بتهيئة هذه الخصائص وقراءتها وكتابتها أكثر تعقيدًا. لذا ، هل من الممكن تجنب هذا التعقيد واستخدام أغلفة الخصائص دون تغييرات في بناء الجملة؟ مع Swift 5.1 ، الإجابة هي نعم.

الطريقة الجديدة:propertyWrapper تعليق توضيحي

يوفر Swift 5.1 حلاً أكثر أناقة لإنشاء أغلفة الملكية ، حيث يُسمح بوضع علامة على غلاف الخاصية بعلامة @propertyWrapper . تحتوي هذه الأغلفة على بنية أكثر إحكاما مقارنة بتلك التقليدية ، مما ينتج عنه كود أكثر إحكاما ومفهوما. يحتوي التعليق التوضيحي على @propertyWrapper على مطلب واحد فقط: يجب أن يحتوي كائن الغلاف على خاصية غير ثابتة تسمى قيمة " wrappedValue ".

 @propertyWrapper struct Email<Value: StringProtocol> { var value: Value? var wrappedValue: Value? { get { return validate(email: value) ? value : nil } set { value = newValue } } private func validate(email: Value?) -> Bool { guard let email = email else { return false } let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" let emailPred = NSPredicate(format:"SELF MATCHES %@", emailRegEx) return emailPred.evaluate(with: email) } }

لتحديد هذه الخاصية المغلفة في الكود ، نحتاج إلى استخدام الصيغة الجديدة.

 @Email var email: String?

لذلك ، قمنا بتمييز الخاصية بالتعليق التوضيحي @ . يجب أن يتطابق نوع الخاصية مع نوع برنامج التضمين "wrapValue". الآن ، يمكنك العمل مع هذه الخاصية كما هو الحال مع الممتلكات العادية.

 email = "[email protected]" print(email) // [email protected] email = "invalid" print(email) // nil

رائع ، يبدو الآن أفضل من النهج القديم. لكن تطبيق الغلاف لدينا له عيب واحد: فهو لا يسمح بقيمة أولية للقيمة المغلفة.

 @Email var email: String? = "[email protected]" //compilation error.

لحل هذه المشكلة ، نحتاج إلى إضافة المُهيئ التالي إلى الغلاف:

 init(wrappedValue value: Value?) { self.value = value }

وهذا كل شيء.

 @Email var email: String? = "[email protected]" print(email) // [email protected] @Email var email: String? = "invalid" print(email) // nil

الكود النهائي للغلاف أدناه:

 @propertyWrapper struct Email<Value: StringProtocol> { var value: Value? init(wrappedValue value: Value?) { self.value = value } var wrappedValue: Value? { get { return validate(email: value) ? value : nil } set { value = newValue } } private func validate(email: Value?) -> Bool { guard let email = email else { return false } let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" let emailPred = NSPredicate(format:"SELF MATCHES %@", emailRegEx) return emailPred.evaluate(with: email) } }

أغلفة شكلي

لنأخذ مثالاً آخر. أنت تكتب لعبة ، ولديك خاصية يتم فيها تخزين نتائج المستخدم. الشرط هو أن هذه القيمة يجب أن تكون أكبر من أو تساوي 0 وأقل من أو تساوي 100. يمكنك تحقيق ذلك باستخدام غلاف الخاصية.

 @propertyWrapper struct Scores { private let minValue = 0 private let maxValue = 100 private var value: Int init(wrappedValue value: Int) { self.value = value } var wrappedValue: Int { get { return max(min(value, maxValue), minValue) } set { value = newValue } } } @Scores var scores: Int = 0

يعمل هذا الرمز لكنه لا يبدو عامًا. لا يمكنك إعادة استخدامه مع قيود مختلفة (ليس 0 و 100). علاوة على ذلك ، يمكنه تقييد قيم الأعداد الصحيحة فقط. سيكون من الأفضل أن يكون لديك غلاف واحد قابل للتكوين يمكنه تقييد أي نوع يتوافق مع البروتوكول القابل للمقارنة. لجعل غلافنا قابلاً للتهيئة ، نحتاج إلى إضافة جميع معلمات التكوين من خلال مُهيئ. إذا كان المُهيئ يحتوي على سمة wrappedValue (القيمة الأولية لخاصيتنا) ، فيجب أن تكون المعلمة الأولى.

 @propertyWrapper struct Constrained<Value: Comparable> { private var range: ClosedRange<Value> private var value: Value init(wrappedValue value: Value, _ range: ClosedRange<Value>) { self.value = value self.range = range } var wrappedValue: Value { get { return max(min(value, range.upperBound), range.lowerBound) } set { value = newValue } } }

لتهيئة خاصية مغلفة ، نحدد جميع سمات التكوين بين قوسين بعد التعليق التوضيحي.

 @Constrained(0...100) var scores: Int = 0

عدد سمات التكوين غير محدود. تحتاج إلى تعريفها بين قوسين بنفس الترتيب كما في المُهيئ.

الوصول إلى الغلاف نفسه

إذا كنت بحاجة إلى الوصول إلى الغلاف نفسه (وليس القيمة المغلفة) ، فأنت بحاجة إلى إضافة شرطة سفلية قبل اسم الخاصية. على سبيل المثال ، لنأخذ هيكل الحساب الخاص بنا.

 struct Account { var firstName: String var lastName: String @Email var email: String? } let account = Account(firstName: "Test", lastName: "Test", email: "[email protected]") account.email // Wrapped value (String) account._email // Wrapper(Email<String>)

نحتاج إلى الوصول إلى الغلاف نفسه لاستخدام الوظيفة الإضافية التي أضفناها إليه. على سبيل المثال ، نريد أن تتوافق بنية الحساب مع بروتوكول Equatable. حسابان متساويان إذا كانت عناوين البريد الإلكتروني الخاصة بهم متساوية ، ويجب أن تكون عناوين البريد الإلكتروني غير حساسة لحالة الأحرف.

 extension Account: Equatable { static func ==(lhs: Account, rhs: Account) -> Bool { return lhs.email?.lowercased() == rhs.email?.lowercased() } }

إنه يعمل ، لكنه ليس الحل الأفضل لأننا يجب أن نتذكر إضافة طريقة () ذات أحرف صغيرة أينما نقارن رسائل البريد الإلكتروني. أفضل طريقة هي جعل بنية البريد الإلكتروني متساوية:

 extension Email: Equatable { static func ==(lhs: Email, rhs: Email) -> Bool { return lhs.wrappedValue?.lowercased() == rhs.wrappedValue?.lowercased() } }

وقارن الأغلفة بدلاً من القيم المغلفة:

 extension Account: Equatable { static func ==(lhs: Account, rhs: Account) -> Bool { return lhs._email == rhs._email } }

القيمة المتوقعة

يوفر التعليق التوضيحي @propertyWrapper واحدًا إضافيًا من مادة السكر التركيبية - وهي القيمة المتوقعة. يمكن أن يكون لهذه الخاصية أي نوع تريده. للوصول إلى هذه الخاصية ، تحتاج إلى إضافة بادئة $ إلى اسم الخاصية. لشرح كيفية عملها ، نستخدم مثالاً من إطار عمل Combine.

ينشئ غلاف الخاصيةPublished ناشرًا للخاصية @Published كقيمة مسقطة.

 @Published var message: String print(message) // Print the wrapped value $message.sink { print($0) } // Subscribe to the publisher

كما ترى ، نستخدم رسالة للوصول إلى الخاصية المغلفة ورسالة $ للوصول إلى الناشر. ما الذي يجب عليك فعله لإضافة قيمة مسقطة إلى الغلاف الخاص بك؟ لا شيء مميز ، فقط أعلنه.

 @propertyWrapper struct Published<Value> { private let subject = PassthroughSubject<Value, Never>() var wrappedValue: Value { didSet { subject.send(wrappedValue) } } var projectedValue: AnyPublisher<Value, Never> { subject.eraseToAnyPublisher() } }

كما ذكرنا سابقًا ، يمكن أن يكون لخاصية projectedValue أي نوع بناءً على احتياجاتك.

محددات

تبدو بنية أغلفة الخصائص الجديدة جيدة ولكنها تحتوي أيضًا على العديد من القيود ، أهمها:

  1. لا يمكنهم المشاركة في معالجة الأخطاء. القيمة المغلفة هي خاصية (وليست طريقة) ، ولا يمكننا تحديد أداة الجمع أو الضبط على أنها throws . على سبيل المثال ، في مثال Email الخاص بنا ، لا يمكن إلقاء خطأ إذا حاول المستخدم تعيين بريد إلكتروني غير صالح. يمكننا إرجاع nil أو تعطل التطبيق fatalError() ، والذي قد يكون غير مقبول في بعض الحالات.
  2. غير مسموح بتطبيق أغلفة متعددة على الخاصية. على سبيل المثال ، سيكون من الأفضل أن يكون لديك غلاف @CaseInsensitive منفصل ودمجه مع غلاف @Email بدلاً من جعل غلاف @Email غير حساس لحالة الأحرف. لكن إنشاءات مثل هذه ممنوعة وتؤدي إلى أخطاء في التجميع.
 @CaseInsensitive @Email var email: String?

كحل بديل لهذه الحالة بالذات ، يمكننا أن نرث غلاف Email من الغلاف CaseInsensitive . ومع ذلك ، فإن الوراثة لها قيود أيضًا - فقط الفئات تدعم الوراثة ، ولا يُسمح إلا بفئة أساسية واحدة.

خاتمة

@propertyWrapper التعليقات التوضيحية علىpropertyWrapper على تبسيط بناء جملة أغلفة الخصائص ، ويمكننا العمل مع الخصائص المغلفة بنفس طريقة التعامل مع الخصائص العادية. هذا يجعل التعليمات البرمجية الخاصة بك ، كمطور Swift أكثر إحكاما وقابلية للفهم. في الوقت نفسه ، هناك العديد من القيود التي يجب أن نأخذها في الاعتبار. آمل أن يتم تصحيح بعضها في إصدارات Swift المستقبلية.

إذا كنت ترغب في معرفة المزيد حول خصائص Swift ، تحقق من المستندات الرسمية.