المسارات
/
Swift
Swift
/
التمارين
/
نظام النوافذ
نظام النوافذ

نظام النوافذ

تمرين تعلّمي

مقدمة

البنى والأصناف

البنى والأصناف من اللبنات الأساسية في برمجة Swift. وكلاهما وسيلة لتجميع البيانات والدوال المترابطة في وحدات وظيفية قائمة بذاتها. وعندما تُعرّف بنيةً أو صنفًا، فأنت تُعرّف نوعًا جديدًا لاستخدامه داخل Swift، تمامًا كما استخدمت الأنواع التي عملت بها من قبل، مثل Int وString.

توجد أوجه تشابه كثيرة بين البنى والأصناف في Swift. فمن بين أوجه التشابه الأخرى، يستطيع كلاهما تخزين القيم في خصائص وتقديم الوظائف من خلال استخدام طرق.

ويقدّم كلٌّ منهما بعض الوظائف الإضافية التي تخرج عن نطاق هذا التمرين.

تعريف البنى والأصناف

تُعرَّف البنى والأصناف بالطريقة نفسها تقريبًا. فهي تبدأ بالكلمة المفتاحية المناسبة، تليها اسم النوع الذي يُعرَّف، ثم يأتي جسم البنية/الصنف موضوعًا بين الأقواس المعقوفة.

قد يتكون الجسم من خصائص مخزنة، تُعرَّف وتتصرّف تمامًا مثل الثوابت أو المتغيرات العادية.

struct CharacterStats {
  var health = 0.0
  var speed = 0
  var strength = 0
}

class GameCharacter {
  var stats = CharacterStats()
  var characterClass: String?
  var name: String?
  var active = false
  let id = makeRandomID()
}

النسخ

كما ذُكر أعلاه، تعريف بنية أو صنف ليس سوى تعريف نوع جديد. وهو مجرد مخطط لما ستبدو عليه قيم ذلك النوع، لكنه لا يُنشئ فعليًا أي قيم من ذلك النوع لتعمل عليها.

ولإنشاء نسخة من ذلك النوع، عليك كتابة اسم النوع متبوعًا بزوج من الأقواس الهلالية.

let someStats = CharacterStats()
let someCharacter = GameCharacter()

سيؤدي هذا إلى إنشاء قيم من هذه الأنواع، حيث تُملأ الخصائص بالقيم الافتراضية المزوّدة في التعريف. لاحظ أنه في الحالات الاختيارية، مثل خاصية name في GameCharacter، وما لم تُزوَّد الخاصية بقيمة، فستكون قيمتها الافتراضية nil، تمامًا كما عند تعريف أنواع اختيارية عادية لا تُزوَّد فيها القيمة على الفور.

وفي حالة البنى، توفر Swift تلقائيًا شيئًا يُسمى مهيّئ الأعضاء، حيث يمكن تزويد قيم خصائص البنية داخل الأقواس الهلالية، فتستبدل القيم الافتراضية الواردة في التعريف.

let differentStats = CharacterStats(health: 100.0, speed: 6, strength: 18)

الوصول إلى الخصائص

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

ويمكن استخدام هذا الترميز لاسترجاع قيمة الخاصية، وكذلك، حيثما كان ذلك مسموحًا، لتغييرها.

someStats.health
// => 0
someCharacter.name
// => nil

someStats.health = 87.3
someStats.health
// => 87.3
someCharacter.name = "Luther"
someCharacter.name
// => "Luther"

someCharacter.id  = "new id"
// Error: Cannot assign to property: 'id' is a 'let' constant

الطرق

وكما هو الحال مع الخصائص التي تخزّن البيانات في بنيتك وأصنافك، يمكنك أيضًا تعريف طرق تخزّن دوال في بنيتك أو صنفك.

تُعرَّف الطرق بالطريقة نفسها التي تُعرَّف بها الدالة العادية، لكن داخل جسم البنية أو الصنف فقط. لاحظ أنه إذا غيّرت دالة قيمة خاصية داخل بنية، فيجب أن تسبقها الكلمة المفتاحية mutating. وإضافةً إلى ذلك، إذا كان بإمكان طريقة تغيير خاصية، فيجب تعريف تلك الخاصية باستخدام var بدلًا من let، تمامًا مثل المتغيرات العادية.

struct CharacterStats {
  var health = 0.0
  var speed = 0
  var strength = 0

  mutating func takeHit(_ damage: Double) {
    health = max(0.0, health - damage)
  }
  func canLift(_ weight: Int) -> Bool {
    weight < strength * 100
  }
}

class GameCharacter {
  var stats = CharacterStats()
  var characterClass: String?
  var name: String?
  var active = false
  let id: String = makeRandomID()

  func takesDamage(_ damage: Double) {
    stats.takeHit(damage)
    if stats.health <= 0 {
      active = false
    }
  }
  func sayName() -> String {
    return "My name is \(name ?? "no one"), my class is \(characterClass ?? "undetermined")"
  }
  func lift(_ weight: Int) -> String {
    if stats.canLift(weight) {
      return "No problem!"
    } else {
      return "Ooof! No way."
    }
  }
}

ويمكن استدعاء هذه الطرق باستخدام الترميز النقطي، تمامًا مثل الخصائص.

var myChar = GameCharacter()
myChar.stats = CharacterStats(health: 72.8, speed: 19, strength: 6)
myChar.active = true

myChar.lift(750)
// => "Ooof! No way."
myChar.takesDamage(80)
myChar.active
// => false

Self

تحتوي كل نسخة من بنية أو صنف على خاصية ضمنية تُسمى self، وتشير إلى النسخة نفسها. والاستخدام الأكثر شيوعًا لـ self هو إزالة الالتباس بين خاصية النسخة ومعامل يحمل الاسم نفسه:

struct Position {
    var x = 0

    mutating func move(x: Int) {
        // Here, 'self.x' refers to the struct's property,
        // while 'x' refers to the parameter passed to the method.
        self.x = x
    }
}

وتوفر أنواع Swift أيضًا كلمة مفتاحية Self بحرف كبير، تشير إلى نوع النسخة بدلًا من النسخة نفسها.

التعليمات

في هذا التمرين، ستحاكي نظامًا حاسوبيًا قائمًا على النوافذ. ستنشئ بعض النوافذ التي يمكن تحريكها وتغيير أحجامها وعرض محتوياتها. الصورة التالية توضّح القيم التي ستعمل عليها أدناه.

                  <--------------------- screenSize.width --------------------->

       ^          ╔════════════════════════════════════════════════════════════╗
       |          ║                                                            ║
       |          ║          position.x,_                                      ║
       |          ║          position.y  \                                     ║
       |          ║                       \<----- size.width ----->            ║
       |          ║                 ^      *──────────────────────┐            ║
       |          ║                 |      │        title         │            ║
       |          ║                 |      ├──────────────────────┤            ║
screenSize.height ║                 |      │                      │            ║
       |          ║            size.height │                      │            ║
       |          ║                 |      │       contents       │            ║
       |          ║                 |      │                      │            ║
       |          ║                 |      │                      │            ║
       |          ║                 v      └──────────────────────┘            ║
       |          ║                                                            ║
       |          ║                                                            ║
       v          ╚════════════════════════════════════════════════════════════╝

1. عرّف بنية Size

عرّف بنية باسم Size لها خاصيتان من النوع Int هما width وheight تخزّنان العرض والارتفاع الحاليين للنافذة على الترتيب. ينبغي أن يكون العرض والارتفاع الابتدائيان 80 و60 على الترتيب. أضف طريقة resize(newWidth:newHeight:) تأخذ معاملَي العرض والارتفاع الجديدين وتغيّر الخصائص لتعكس الحجم الجديد.

let size1080x764 = Size(width: 1080, height: 764)
// Returns Size
var size1200x800 = size1080x764
// Returns Size
size1200x800.resize(newWidth: 1200, newHeight: 800)
size1200x800.height
// Returns 800

2. عرّف بنية Position

عرّف بنية باسم Position لها خاصيتان من النوع Int هما x وy تخزّنان الموضع الأفقي والرأسي الحاليين، على الترتيب، للزاوية العلوية اليسرى للنافذة. ينبغي أن تكون القيمة الابتدائية لكل من x و y هي 0. الموضع (0، 0) هو الزاوية العلوية اليسرى للشاشة، وتزداد قيم x كلما تحرّكت يمينًا وتزداد قيم y كلما تحرّكت لأسفل.

أضف طريقة moveTo(newX:newY:) تأخذ معاملَي x و y الجديدين وتغيّر الخصائص لتعكس الموضع الجديد.

var point = Position(x: 10, y: 20)
// Returns Position
point.moveTo(newX: 100, newY: -100)
point.y
// Returns -100

3. عرّف صنف Window

عرّف صنف نافذة بالخصائص التالية:

  • title : String، القيمة الابتدائية هي "New Window"
  • screenSize : Size، قيمة ثابتة حيث width = 800 وheight = 600
  • size : Size، القيمة الابتدائية هي القيمة الافتراضية لبنية Size
  • position : Position، القيمة الابتدائية هي القيمة الافتراضية لبنية Position
  • contents : String?، القيمة الابتدائية هي nil

ينبغي أيضًا أن تعرّف مُهيّئًا فارغًا للصنف.

let window = Window()
window.title
// Returns "New Window"

4. أضف طريقة لتغيير حجم النوافذ

  • resize(to:) : (Size) -> () - تأخذ هذه الطريقة بنية Size كمدخل وتحاول تغيير حجم النافذة إلى الحجم المحدد. ومع ذلك، لا يمكن للحجم الجديد أن يتجاوز حدودًا معينة.
  • أصغر ارتفاع أو عرض مسموح به هو 1. أي ارتفاع أو عرض مطلوب أقل من 1 سيُقلَّص إلى 1.
  • يعتمد الحد الأقصى للارتفاع والعرض على الموضع الحالي للنافذة؛ فلا يمكن لحواف النافذة أن تتجاوز حواف الشاشة. القيم الأكبر من هذه الحدود ستُقلَّص إلى أكبر حجم يمكن أن تتخذه. على سبيل المثال، إذا كان موضع النافذة عند x = 400، y = 300 وطُلب تغيير الحجم إلى height = 400، width = 300، فسيُغيَّر حجم النافذة إلى height = 300، width = 300 لأن الشاشة ليست كبيرة بما يكفي في اتجاه y لاستيعاب الطلب بالكامل.

5. أضف طريقة لتحريك النوافذ

  • move(to:) : (Position) -> () - هذه الطريقة مشابهة لـ resize(to:)، لكنها تضبط موضع النافذة إلى القيمة المطلوبة بدلًا من الحجم. وكما في resize، لا يمكن للموضع الجديد أن يتجاوز حدودًا معينة.
  • أصغر موضع هو 0 لكل من x وy.
  • يعتمد الموضع الأقصى في أي من الاتجاهين على الحجم الحالي للنافذة؛ فلا يمكن للحواف أن تتجاوز حواف الشاشة. القيم الأكبر من هذه الحدود ستُقلَّص إلى أكبر حجم يمكن أن تتخذه. على سبيل المثال، إذا كان حجم النافذة عند x = 250، y = 100 وطُلب الانتقال إلى x = 600، y = 200، فستُحرَّك النافذة إلى x = 550، y = 200 لأن الشاشة ليست كبيرة بما يكفي في اتجاه x لاستيعاب الطلب بالكامل.

6. أضف طرقًا لتحديث نص النافذة وعرض معلومات النافذة

  • update(title:) : (String) -> () - تضبط هذه الطريقة خاصية title على قيمة السلسلة النصية التي مُرِّرت.
  • update(text:) : (String?) -> () - تضبط هذه الطريقة خاصية contents على قيمة السلسلة النصية الاختيارية التي مُرِّرت.
  • display() : () -> String - تُرجع هذه الطريقة سلسلة نصية تصف الحالة الحالية للنافذة. على سبيل المثال، إذا كانت النافذة تحمل العنوان title بالقيمة "My First Window" مع الموضع: x = 10، y = 100؛ والحجم: width = 200، height = 150؛ والمحتويات: "I 😍 my window"، فينبغي أن تُرجع السلسلة النصية: "My First Window\nPosition: (10, 100), Size: (200 x 150)\nI 😍 my window\n" - إذا كانت contents تساوي nil، فينبغي أن يكون السطر الأخير "[This window intentionally left blank]"

7. أنشئ مُهيّئًا لصنف Window

ينبغي أن يكون لنظام النوافذ مُهيّئ بحيث يمكن إنشاء النافذة بمدخلات مخصصة. أنشئ مُهيّئًا آخر لصنف Window. ينبغي أن يأخذ المُهيّئ المعاملات التالية: title وcontents، ومعاملين اختياريين هما size وposition. ينبغي أن تكون القيمة الافتراضية لـ size وposition هي القيم الافتراضية لبنيتَي Size وPosition.

let window = Window(title: "My First Window", contents: "I 😍 my window")
window.display()
// Returns "My First Window\nPosition: (0, 0), Size: (80 x 60)\nI 😍 my window\n"

let window2 = Window(title: "My Second Window", contents: "I 😍 my window", size: Size(width: 200, height: 150), position: Position(x: 10, y: 100))
window2.display()
// Returns "My Second Window\nPosition: (10, 100), Size: (200 x 150)\nI 😍 my window\n"
تعديل عبر GitHub يفتح الرابط في نافذة أو علامة تبويب جديدة
Swift Exercism

مستعد لبدء نظام النوافذ؟

سجّل في Exercism لتتعلّم وتتقن Swift عبر 35 مفهومًا116 تمرينًا، وإرشاد بشري حقيقي، وكل ذلك مجانًا.