پاکسازی و بازسازی کدهای SwiftUI | به زبان ساده

۴۱ بازدید
آخرین به‌روزرسانی: ۰۹ مهر ۱۴۰۲
زمان مطالعه: ۷ دقیقه
پاکسازی و بازسازی کدهای SwiftUI | به زبان ساده

در این مقاله با معرفی یک مثال به بررسی روش پاکسازی و بازسازی کدهای SwiftUI به صورت عملی می‌پردازیم. در این راهنما از نسخه بتای Xcode با SwiftUI 2.0 استفاده می‌کنیم. اگر شما نیز می‌خواهید مراحل این راهنما را به صورت عملی دنبال کنید باید مطمئن شوید که از همین نسخه‌ها استفاده می‌کنید.

فهرست مطالب این نوشته

مقدمه

تلاش خواهیم کرد مثالی که در این راهنما بررسی می‌کنیم کاملاً ساده و سرراست باشد، چون در هر حال این یک راهنما است. در این مثال 5 باکس داریم که یک باکس بزرگ در مرکز و چهار باکس کوچک‌تر در چهار گوشه صفحه قرار دارند. زمانی که روی یکی از باکس‌های کوچک کلیک کنیم، هر پنج باکس در آن جهت حرکت می‌کنند. برای درک بهتر به تصویر زیر توجه کنید:

پاکسازی و بازسازی کدهای SwiftUI

راه‌حل

پیاده‌سازی این مثال ساده به نظر می‌رسد.

در ادامه پیش‌نویس اولیه کد برای انجام کارهای فوق را مشاهده می‌کنید. این یک پیاده‌سازی سریع و ساده برای اجرایی کردن ایده است. توجه کنید که بدنه حلقه اصلی شامل 45 خط کد است. در ادامه باید این بخش را «بازسازی» (refactor) کنیم:

1import SwiftUI
2
3struct SwiftUIViewG1: View {
4    @State var offSetX: CGFloat = 0
5    @State var offsetY: CGFloat = 0
6    var body: some View {
7      ZStack {
8        ZStack {
9          Rectangle()
10            .stroke(Color.black, lineWidth: 2)
11            .frame(width: 128, height: 128)
12        }
13        ZStack {
14          Rectangle()
15            .stroke(Color.red, lineWidth: 2)
16            .frame(width: 32, height: 32)
17            .onTapGesture {
18              offSetX -= 10; offsetY -= 10
19            }
20        }.frame(width: 256, height: 256, alignment: .topLeading)
21        ZStack {
22          Rectangle()
23            .stroke(Color.blue, lineWidth: 2)
24            .frame(width: 32, height: 32)
25            .onTapGesture {
26              offSetX += 10; offsetY -= 10
27            }
28        }.frame(width: 256, height: 256, alignment: .topTrailing)
29        ZStack {
30          Rectangle()
31            .stroke(Color.green, lineWidth: 2)
32            .frame(width: 32, height: 32)
33            .onTapGesture {
34              offSetX -= 10; offsetY += 10
35              
36            }
37        }.frame(width: 256, height: 256, alignment: .bottomLeading)
38        ZStack {
39          Rectangle()
40            .stroke(Color.yellow, lineWidth: 2)
41            .frame(width: 32, height: 32)
42            .onTapGesture {
43              offSetX += 10; offsetY += 10
44              
45            }
46        }.frame(width: 256, height: 256, alignment: .bottomTrailing)
47      }.offset(x:offSetX, y:offsetY)
48    }
49}
50
51struct SwiftUIViewG1_Previews: PreviewProvider {
52    static var previews: some View {
53        SwiftUIViewG1()
54    }
55}

در کد فوق می‌بینیم که پنج مستطیل درون تعدادی ZStacks ایجاد شده و به تعدادی ZStacks اضافه شده‌اند تا باکس‌ها بتوانند در زمان تپ شدن، جابجا شوند. اکنون تلاش می‌کنیم تا این کد را بهبود ببخشیم.

یکی از نخستین کارهایی که باید انجام دهیم، این است که مطمئن شویم از اعداد جادویی یعنی «ارقام لفظی» (literal figures) استفاده نمی‌کنیم. این موارد را در کد با ثوابت تعویض می‌کنیم:

1import SwiftUI
2
3struct SwiftUIViewG2: View {
4    let bigBox:CGFloat = 256
5    let mediumBox:CGFloat = 128
6    let smallBox:CGFloat = 32
7    let lineWidth:CGFloat = 2
8    let xOffsetToBigBox:CGFloat = 10
9    let yOffsetToBigBox:CGFloat = 10
10    
11    @State var bigBoxOffset = CGSize(width: 0, height: 0)
12    @State var offSetX: CGFloat = 0
13    @State var offsetY: CGFloat = 0
14
15    var body: some View {
16      ZStack {
17        ZStack {
18          Rectangle()
19            .stroke(Color.black, lineWidth: lineWidth)
20            .frame(width: mediumBox, height: mediumBox)
21        }
22        
23        ZStack {
24          Rectangle()
25            .stroke(Color.red, lineWidth: lineWidth)
26            .frame(width: smallBox, height: smallBox)
27            .onTapGesture {
28              offSetX -= xOffsetToBigBox
29              offsetY -= yOffsetToBigBox
30            }
31        }.frame(width: bigBox, height: bigBox, alignment: .topLeading)
32        
33        ZStack {
34          Rectangle()
35            .stroke(Color.blue, lineWidth: lineWidth)
36            .frame(width: smallBox, height: smallBox)
37            .onTapGesture {
38              offSetX += xOffsetToBigBox
39              offsetY -= yOffsetToBigBox
40            }
41        }.frame(width: bigBox, height: bigBox, alignment: .topTrailing)
42        
43        ZStack {
44          Rectangle()
45            .stroke(Color.green, lineWidth: lineWidth)
46            .frame(width: smallBox, height: smallBox)
47            .onTapGesture {
48              offSetX -= xOffsetToBigBox
49              offsetY += yOffsetToBigBox
50            }
51        }.frame(width: bigBox, height: bigBox, alignment: .bottomLeading)
52        
53        ZStack {
54          Rectangle()
55            .stroke(Color.yellow, lineWidth: lineWidth)
56            .frame(width: smallBox, height: smallBox)
57            .onTapGesture {
58              offSetX += xOffsetToBigBox
59              offsetY += yOffsetToBigBox
60            }
61        }.frame(width: bigBox, height: bigBox, alignment: .bottomTrailing)
62      }.offset(x:offSetX, y:offsetY)
63    }
64}
65
66struct SwiftUIViewG2_Previews: PreviewProvider {
67    static var previews: some View {
68        SwiftUIViewG2()
69    }
70}

این کد ظاهر بسیار بهتری دارد. کد فوق با حذف همه اعدادی که در سراسر کد پخش شده بودند، خوانایی بهتری یافته است. به علاوه اکنون می‌توانیم معنی اعداد را بهتر درک کنیم. در ادامه با بررسی کد می‌بینیم که بخش زیادی از کد تکراری است. ما می‌توانیم بخش رسم مجدد باکس‌ها را فاکتور بگیریم و یک ساختار برای فعال‌سازی جستچرها معرفی کنیم:

1import SwiftUI
2
3struct SwiftUIViewG3: View {
4    let bigBox:CGFloat = 256
5    
6    let smallBox:CGFloat = 32
7    let xOffsetToBigBox:CGFloat = 10
8    let yOffsetToBigBox:CGFloat = 10
9    
10    @State var offsetsToBigBox = CGSize(width: 10, height: 10)
11    
12    @State var redOffset = CGSize(width: -10, height: -10)
13    @State var blueOffset = CGSize(width: +10, height: -10)
14    @State var greenOffset = CGSize(width: -10, height: +10)
15    @State var yellowOffset = CGSize(width: +10, height: +10)
16    
17    @State var bigBoxOffset = CGSize(width: 0, height: 0)
18    @State var lineWidth:CGFloat = 2
19
20    var body: some View {
21      ZStack {
22        ZStack {
23          drawMBoxRectangleG3(lineColor: Color.black, lineWidth: $lineWidth)
24        }
25        
26        ZStack {
27          drawSBoxRectangleG3(lineColor: Color.red, lineWidth: $lineWidth, bigBoxOffset: $bigBoxOffset, offsetsToBigBox: $redOffset)
28        }.frame(width: bigBox, height: bigBox, alignment: .topLeading)
29        
30        ZStack {
31          drawSBoxRectangleG3(lineColor: Color.blue, lineWidth: $lineWidth, bigBoxOffset: $bigBoxOffset, offsetsToBigBox: $blueOffset)
32        }.frame(width: bigBox, height: bigBox, alignment: .topTrailing)
33        
34        ZStack {
35          drawSBoxRectangleG3(lineColor: Color.green, lineWidth: $lineWidth, bigBoxOffset: $bigBoxOffset, offsetsToBigBox: $greenOffset)
36        }.frame(width: bigBox, height: bigBox, alignment: .bottomLeading)
37        
38        ZStack {
39          drawSBoxRectangleG3(lineColor: Color.yellow, lineWidth: $lineWidth, bigBoxOffset: $bigBoxOffset, offsetsToBigBox: $yellowOffset)
40        }.frame(width: bigBox, height: bigBox, alignment: .bottomTrailing)
41      }
42       .offset(bigBoxOffset)
43    }
44}
45
46struct drawSBoxRectangleG3: View {
47  let smallBox:CGFloat = 32
48
49  @State var lineColor: Color
50  @Binding var lineWidth: CGFloat
51  @Binding var bigBoxOffset: CGSize
52  @Binding var offsetsToBigBox:CGSize
53
54  var body: some View {
55    Rectangle()
56      .stroke(lineColor, lineWidth: lineWidth)
57      .frame(width: smallBox, height: smallBox)
58      .onTapGesture {
59          bigBoxOffset.width += offsetsToBigBox.width
60          bigBoxOffset.height += offsetsToBigBox.height
61      }
62  }
63}
64
65struct drawMBoxRectangleG3: View {
66  let mediumBox:CGFloat = 128
67
68  @State var lineColor: Color
69  @Binding var lineWidth: CGFloat
70
71  var body: some View {
72    Rectangle()
73      .stroke(lineColor, lineWidth: lineWidth)
74      .frame(width: mediumBox, height: mediumBox)
75  }
76}
77
78
79struct SwiftUIViewG3_Previews: PreviewProvider {
80    static var previews: some View {
81        SwiftUIViewG3()
82    }
83}

اینک کد ظاهر بسیار بهتری یافته است. بدنه کد اکنون به 25 خط کاهش یافته است. آیا بهبود دیگری وجود دارد که بتوان اجرا کرد؟ واقعیت این است که فرمول‌های ریاضی زیادی برای جابجا کردن باکس‌ها به همراه متغیرهای ‎@State استفاده شده است. باید کاری کنیم که این بخش کمی سرراست‌تر شود، بنابراین یک enum اضافه کرده و کد را درون خود متد قرار می‌دهیم. با استفاده از یک enum می‌توانیم اتفاقاتی که می‌افتد را به صورت روشن‌تری به نمایش بگذاریم.

همچنین متدهایی که به رسم باکس‌ها درون دامنه struct اصلی مربوط می‌شوند را جابجا کرده‌ایم و در آغاز کار سطح بالاتری اعلان نموده‌ایم:

1import SwiftUI
2
3struct SwiftUIViewG4: View {
4
5    enum boxMove {
6      case topLeft
7      case topRight
8      case bottomLeft
9      case bottomRight
10    }
11    
12    let shiftBox: boxMove
13    let bigBox:CGFloat = 256
14    
15    @State var bigBoxOffset = CGSize(width: 0, height: 0)
16    @State var lineWidth:CGFloat = 2
17    
18    struct drawSBoxRectangleG4: View {
19      let smallBox:CGFloat = 32
20      let xOffsetToBigBox:CGFloat = 10
21      let yOffsetToBigBox:CGFloat = 10
22      let lineWidth:CGFloat = 2
23      
24      @State var lineColor: Color
25      @State var boxDirection: boxMove
26      @Binding var bigBoxOffset: CGSize
27      
28      var body: some View {
29        Rectangle()
30          .stroke(lineColor, lineWidth: lineWidth)
31          .frame(width: smallBox, height: smallBox)
32          .onTapGesture {
33            switch boxDirection {
34              case .topLeft:
35                bigBoxOffset.width -= xOffsetToBigBox
36                bigBoxOffset.height -= yOffsetToBigBox
37              case .topRight:
38                bigBoxOffset.width += xOffsetToBigBox
39                bigBoxOffset.height -= yOffsetToBigBox
40              case .bottomLeft:
41                bigBoxOffset.width -= xOffsetToBigBox
42                bigBoxOffset.height += yOffsetToBigBox
43              case .bottomRight:
44                bigBoxOffset.width += xOffsetToBigBox
45                bigBoxOffset.height += yOffsetToBigBox
46            }
47          }
48        }
49      }
50        
51      struct drawMBoxRectangleG4: View {
52        let mediumBox:CGFloat = 128
53        let lineWidth:CGFloat = 2
54        
55        @State var lineColor: Color
56        
57        var body: some View {
58          Rectangle()
59            .stroke(lineColor, lineWidth: lineWidth)
60            .frame(width: mediumBox, height: mediumBox)
61        }
62      }
63
64    var body: some View {
65      ZStack {
66        ZStack {
67          drawMBoxRectangleG4(lineColor: Color.black)
68        }
69        
70        ZStack {
71          drawSBoxRectangleG4(lineColor: Color.red, boxDirection: .topLeft, bigBoxOffset: $bigBoxOffset)
72        }.frame(width: bigBox, height: bigBox, alignment: .topLeading)
73        
74        ZStack {
75          drawSBoxRectangleG4(lineColor: Color.blue, boxDirection: .topRight, bigBoxOffset: $bigBoxOffset)
76        }.frame(width: bigBox, height: bigBox, alignment: .topTrailing)
77        
78        ZStack {
79          drawSBoxRectangleG4(lineColor: Color.green, boxDirection: .bottomLeft, bigBoxOffset: $bigBoxOffset)
80        }.frame(width: bigBox, height: bigBox, alignment: .bottomLeading)
81        
82        ZStack {
83          drawSBoxRectangleG4(lineColor: Color.yellow, boxDirection: .bottomRight,  bigBoxOffset: $bigBoxOffset)
84        }.frame(width: bigBox, height: bigBox, alignment: .bottomTrailing)
85      }.offset(bigBoxOffset)
86  }
87}
88
89struct SwiftUIViewG4_Previews: PreviewProvider {
90    static var previews: some View {
91      SwiftUIViewG4(shiftBox: .topLeft)
92    }
93}

این کد کمی بهتر شد، اما حجم حلقه اصلی را چندان کاهش نداد. در گام بعدی باید کپسوله‌سازی را به دقت تنظیم کنیم. به این منظور public و همچنین private را اضافه می‌کنیم تا کدها کاملاً روشن شوند.

همچنین «جهت‌گیری» (alignment) که سمت حرکت را مشخص می‌کرد را نیز حذف کردیم، زیرا می‌خواهیم یک حلقه در این فرایند تعریف کنیم و تعداد پارامترهایی که باید ارسال شوند را کاهش دهیم. این alignment را می‌توان به صورت مستقیم روی direction نگاشت کرد و این دقیقاً همان کاری است که در کد زیر انجام داده‌ایم:

1import SwiftUI
2
3struct SwiftUIViewG5: View {
4  
5  public enum boxMove {
6    case topLeft
7    case topRight
8    case bottomLeft
9    case bottomRight
10  }
11  
12  public let shiftBox: boxMove
13  private let bigBox:CGFloat = 256
14  
15  @State private var bigBoxOffset = CGSize(width: 0, height: 0)
16  @State private var lineWidth:CGFloat = 2
17  
18  private struct drawSBoxRectangleG5: View {
19    
20    private let bigBox:CGFloat = 256
21    private let smallBox:CGFloat = 32
22    private let xOffsetToBigBox:CGFloat = 10
23    private let yOffsetToBigBox:CGFloat = 10
24    private let lineWidth:CGFloat = 2
25    
26    
27    @State public var lineColor: Color
28    @State public var boxDirection: boxMove
29    @State private var alignToCorner: Alignment = .center
30    
31    @Binding public var bigBoxOffset: CGSize
32    
33    var body: some View {
34      ZStack {
35        Rectangle()
36          .stroke(lineColor, lineWidth: lineWidth)
37          .frame(width: smallBox, height: smallBox)
38          .onTapGesture {
39            switch boxDirection {
40            case .topLeft:
41              bigBoxOffset.width -= xOffsetToBigBox
42              bigBoxOffset.height -= yOffsetToBigBox
43            case .topRight:
44              bigBoxOffset.width += xOffsetToBigBox
45              bigBoxOffset.height -= yOffsetToBigBox
46            case .bottomLeft:
47              bigBoxOffset.width -= xOffsetToBigBox
48              bigBoxOffset.height += yOffsetToBigBox
49            case .bottomRight:
50              bigBoxOffset.width += xOffsetToBigBox
51              bigBoxOffset.height += yOffsetToBigBox
52            }
53          }.onAppear( perform: {
54            switch boxDirection {
55            case .topLeft:
56              alignToCorner = .topLeading
57            case .topRight:
58              alignToCorner = .topTrailing
59            case .bottomLeft:
60              alignToCorner = .bottomLeading
61            case .bottomRight:
62              alignToCorner = .bottomTrailing
63            }
64          })
65      }.frame(width: bigBox, height: bigBox, alignment: alignToCorner)
66    }
67  }
68  
69  private struct drawMBoxRectangleG5: View {
70    
71    private let mediumBox:CGFloat = 128
72    private let lineWidth:CGFloat = 2
73    
74    @State public var lineColor: Color
75    
76    var body: some View {
77      Rectangle()
78        .stroke(lineColor, lineWidth: lineWidth)
79        .frame(width: mediumBox, height: mediumBox)
80    }
81  }
82  
83  struct align: Hashable {
84    var id = UUID()
85    var directionToHead: boxMove
86  }
87  
88  var corners:[align] = [align(directionToHead: .topLeft),
89                         align(directionToHead: .topRight),
90                         align(directionToHead: .bottomLeft),
91                         align(directionToHead: .bottomRight)]
92  
93  var body: some View {
94    ZStack {
95      ZStack {
96        drawMBoxRectangleG5(lineColor: Color.black)
97      }
98      
99      ForEach(corners, id: \.self) { corner in
100        drawSBoxRectangleG5(lineColor: Color.red, boxDirection: corner.directionToHead, bigBoxOffset: $bigBoxOffset)
101      }
102      
103    }.offset(bigBoxOffset)
104  }
105}
106
107struct SwiftUIViewG5_Previews: PreviewProvider {
108  static var previews: some View {
109    SwiftUIViewG5(shiftBox: .topLeft)
110  }
111}

سخن پایانی

به این ترتیب به انتهای این راهنما می‌رسیم. با این که ما کد را با 55 خط آغاز کرده و به در نهایت به کدی با 110 خط رسیدیم، اما تقریباً همه خطوط کد اولیه در حلقه اصلی قرار داشتند. این حلقه اصلی بیش از یک صفحه منفرد نمایشگر را اشغال می‌کند. به طور عکس حلقه اصلی اینک شامل صرفاً 11 خط کد است و منطق آن بسیار ساده‌تر شده و درک آن نیز به آسان‌تر صورت می‌گیرد.

نکته آخری که باید مورد اشاره قرا دهیم، این است که ما تلاش کردیم var و align را به صورت struct خصوصی درآوریم. اما در این حالت امکان کامپایل وجود نداشت، زیرا بخش پیش‌نمایش در مورد سطح حفاظت خطا می‌داد. بنابراین preview را کامنت کردیم تا بتوانیم این گام آخر را اجرا کنیم. متأسفانه این مشکل نشان می‌دهد که بخش preview مشکلاتی دارد.

بر اساس رای ۰ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
better-programming
نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *