كيفية الاقتراب من أغلفة خصائص Swift
نشرت: 2022-03-11بعبارات بسيطة ، غلاف الخاصية هو بنية عامة تغلف القراءة والكتابة للخاصية وتضيف سلوكًا إضافيًا إليها. نستخدمه إذا احتجنا إلى تقييد قيم الخصائص المتاحة ، أو إضافة منطق إضافي إلى الوصول للقراءة / الكتابة (مثل استخدام قواعد البيانات أو الإعدادات الافتراضية للمستخدم) ، أو إضافة بعض الطرق الإضافية.
تتناول هذه المقالة طريقة 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?
لذلك ، قمنا بتمييز الخاصية بالتعليق التوضيحي @
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
أي نوع بناءً على احتياجاتك.
محددات
تبدو بنية أغلفة الخصائص الجديدة جيدة ولكنها تحتوي أيضًا على العديد من القيود ، أهمها:
- لا يمكنهم المشاركة في معالجة الأخطاء. القيمة المغلفة هي خاصية (وليست طريقة) ، ولا يمكننا تحديد أداة الجمع أو الضبط على أنها
throws
. على سبيل المثال ، في مثالEmail
الخاص بنا ، لا يمكن إلقاء خطأ إذا حاول المستخدم تعيين بريد إلكتروني غير صالح. يمكننا إرجاعnil
أو تعطل التطبيقfatalError()
، والذي قد يكون غير مقبول في بعض الحالات. - غير مسموح بتطبيق أغلفة متعددة على الخاصية. على سبيل المثال ، سيكون من الأفضل أن يكون لديك غلاف
@CaseInsensitive
منفصل ودمجه مع غلاف@Email
بدلاً من جعل غلاف@Email
غير حساس لحالة الأحرف. لكن إنشاءات مثل هذه ممنوعة وتؤدي إلى أخطاء في التجميع.
@CaseInsensitive @Email var email: String?
كحل بديل لهذه الحالة بالذات ، يمكننا أن نرث غلاف Email
من الغلاف CaseInsensitive
. ومع ذلك ، فإن الوراثة لها قيود أيضًا - فقط الفئات تدعم الوراثة ، ولا يُسمح إلا بفئة أساسية واحدة.
خاتمة
@propertyWrapper
التعليقات التوضيحية علىpropertyWrapper على تبسيط بناء جملة أغلفة الخصائص ، ويمكننا العمل مع الخصائص المغلفة بنفس طريقة التعامل مع الخصائص العادية. هذا يجعل التعليمات البرمجية الخاصة بك ، كمطور Swift أكثر إحكاما وقابلية للفهم. في الوقت نفسه ، هناك العديد من القيود التي يجب أن نأخذها في الاعتبار. آمل أن يتم تصحيح بعضها في إصدارات Swift المستقبلية.
إذا كنت ترغب في معرفة المزيد حول خصائص Swift ، تحقق من المستندات الرسمية.