برنامه نویسی 122 بازدید

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

مقدمه

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

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

راه‌حل

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

import SwiftUI

struct SwiftUIViewG1: View {
    @State var offSetX: CGFloat = 0
    @State var offsetY: CGFloat = 0
    var body: some View {
      ZStack {
        ZStack {
          Rectangle()
            .stroke(Color.black, lineWidth: 2)
            .frame(width: 128, height: 128)
        }
        ZStack {
          Rectangle()
            .stroke(Color.red, lineWidth: 2)
            .frame(width: 32, height: 32)
            .onTapGesture {
              offSetX -= 10; offsetY -= 10
            }
        }.frame(width: 256, height: 256, alignment: .topLeading)
        ZStack {
          Rectangle()
            .stroke(Color.blue, lineWidth: 2)
            .frame(width: 32, height: 32)
            .onTapGesture {
              offSetX += 10; offsetY -= 10
            }
        }.frame(width: 256, height: 256, alignment: .topTrailing)
        ZStack {
          Rectangle()
            .stroke(Color.green, lineWidth: 2)
            .frame(width: 32, height: 32)
            .onTapGesture {
              offSetX -= 10; offsetY += 10
              
            }
        }.frame(width: 256, height: 256, alignment: .bottomLeading)
        ZStack {
          Rectangle()
            .stroke(Color.yellow, lineWidth: 2)
            .frame(width: 32, height: 32)
            .onTapGesture {
              offSetX += 10; offsetY += 10
              
            }
        }.frame(width: 256, height: 256, alignment: .bottomTrailing)
      }.offset(x:offSetX, y:offsetY)
    }
}

struct SwiftUIViewG1_Previews: PreviewProvider {
    static var previews: some View {
        SwiftUIViewG1()
    }
}

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

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

import SwiftUI

struct SwiftUIViewG2: View {
    let bigBox:CGFloat = 256
    let mediumBox:CGFloat = 128
    let smallBox:CGFloat = 32
    let lineWidth:CGFloat = 2
    let xOffsetToBigBox:CGFloat = 10
    let yOffsetToBigBox:CGFloat = 10
    
    @State var bigBoxOffset = CGSize(width: 0, height: 0)
    @State var offSetX: CGFloat = 0
    @State var offsetY: CGFloat = 0

    var body: some View {
      ZStack {
        ZStack {
          Rectangle()
            .stroke(Color.black, lineWidth: lineWidth)
            .frame(width: mediumBox, height: mediumBox)
        }
        
        ZStack {
          Rectangle()
            .stroke(Color.red, lineWidth: lineWidth)
            .frame(width: smallBox, height: smallBox)
            .onTapGesture {
              offSetX -= xOffsetToBigBox
              offsetY -= yOffsetToBigBox
            }
        }.frame(width: bigBox, height: bigBox, alignment: .topLeading)
        
        ZStack {
          Rectangle()
            .stroke(Color.blue, lineWidth: lineWidth)
            .frame(width: smallBox, height: smallBox)
            .onTapGesture {
              offSetX += xOffsetToBigBox
              offsetY -= yOffsetToBigBox
            }
        }.frame(width: bigBox, height: bigBox, alignment: .topTrailing)
        
        ZStack {
          Rectangle()
            .stroke(Color.green, lineWidth: lineWidth)
            .frame(width: smallBox, height: smallBox)
            .onTapGesture {
              offSetX -= xOffsetToBigBox
              offsetY += yOffsetToBigBox
            }
        }.frame(width: bigBox, height: bigBox, alignment: .bottomLeading)
        
        ZStack {
          Rectangle()
            .stroke(Color.yellow, lineWidth: lineWidth)
            .frame(width: smallBox, height: smallBox)
            .onTapGesture {
              offSetX += xOffsetToBigBox
              offsetY += yOffsetToBigBox
            }
        }.frame(width: bigBox, height: bigBox, alignment: .bottomTrailing)
      }.offset(x:offSetX, y:offsetY)
    }
}

struct SwiftUIViewG2_Previews: PreviewProvider {
    static var previews: some View {
        SwiftUIViewG2()
    }
}

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

import SwiftUI

struct SwiftUIViewG3: View {
    let bigBox:CGFloat = 256
    
    let smallBox:CGFloat = 32
    let xOffsetToBigBox:CGFloat = 10
    let yOffsetToBigBox:CGFloat = 10
    
    @State var offsetsToBigBox = CGSize(width: 10, height: 10)
    
    @State var redOffset = CGSize(width: -10, height: -10)
    @State var blueOffset = CGSize(width: +10, height: -10)
    @State var greenOffset = CGSize(width: -10, height: +10)
    @State var yellowOffset = CGSize(width: +10, height: +10)
    
    @State var bigBoxOffset = CGSize(width: 0, height: 0)
    @State var lineWidth:CGFloat = 2

    var body: some View {
      ZStack {
        ZStack {
          drawMBoxRectangleG3(lineColor: Color.black, lineWidth: $lineWidth)
        }
        
        ZStack {
          drawSBoxRectangleG3(lineColor: Color.red, lineWidth: $lineWidth, bigBoxOffset: $bigBoxOffset, offsetsToBigBox: $redOffset)
        }.frame(width: bigBox, height: bigBox, alignment: .topLeading)
        
        ZStack {
          drawSBoxRectangleG3(lineColor: Color.blue, lineWidth: $lineWidth, bigBoxOffset: $bigBoxOffset, offsetsToBigBox: $blueOffset)
        }.frame(width: bigBox, height: bigBox, alignment: .topTrailing)
        
        ZStack {
          drawSBoxRectangleG3(lineColor: Color.green, lineWidth: $lineWidth, bigBoxOffset: $bigBoxOffset, offsetsToBigBox: $greenOffset)
        }.frame(width: bigBox, height: bigBox, alignment: .bottomLeading)
        
        ZStack {
          drawSBoxRectangleG3(lineColor: Color.yellow, lineWidth: $lineWidth, bigBoxOffset: $bigBoxOffset, offsetsToBigBox: $yellowOffset)
        }.frame(width: bigBox, height: bigBox, alignment: .bottomTrailing)
      }
       .offset(bigBoxOffset)
    }
}

struct drawSBoxRectangleG3: View {
  let smallBox:CGFloat = 32

  @State var lineColor: Color
  @Binding var lineWidth: CGFloat
  @Binding var bigBoxOffset: CGSize
  @Binding var offsetsToBigBox:CGSize

  var body: some View {
    Rectangle()
      .stroke(lineColor, lineWidth: lineWidth)
      .frame(width: smallBox, height: smallBox)
      .onTapGesture {
          bigBoxOffset.width += offsetsToBigBox.width
          bigBoxOffset.height += offsetsToBigBox.height
      }
  }
}

struct drawMBoxRectangleG3: View {
  let mediumBox:CGFloat = 128

  @State var lineColor: Color
  @Binding var lineWidth: CGFloat

  var body: some View {
    Rectangle()
      .stroke(lineColor, lineWidth: lineWidth)
      .frame(width: mediumBox, height: mediumBox)
  }
}


struct SwiftUIViewG3_Previews: PreviewProvider {
    static var previews: some View {
        SwiftUIViewG3()
    }
}

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

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

import SwiftUI

struct SwiftUIViewG4: View {

    enum boxMove {
      case topLeft
      case topRight
      case bottomLeft
      case bottomRight
    }
    
    let shiftBox: boxMove
    let bigBox:CGFloat = 256
    
    @State var bigBoxOffset = CGSize(width: 0, height: 0)
    @State var lineWidth:CGFloat = 2
    
    struct drawSBoxRectangleG4: View {
      let smallBox:CGFloat = 32
      let xOffsetToBigBox:CGFloat = 10
      let yOffsetToBigBox:CGFloat = 10
      let lineWidth:CGFloat = 2
      
      @State var lineColor: Color
      @State var boxDirection: boxMove
      @Binding var bigBoxOffset: CGSize
      
      var body: some View {
        Rectangle()
          .stroke(lineColor, lineWidth: lineWidth)
          .frame(width: smallBox, height: smallBox)
          .onTapGesture {
            switch boxDirection {
              case .topLeft:
                bigBoxOffset.width -= xOffsetToBigBox
                bigBoxOffset.height -= yOffsetToBigBox
              case .topRight:
                bigBoxOffset.width += xOffsetToBigBox
                bigBoxOffset.height -= yOffsetToBigBox
              case .bottomLeft:
                bigBoxOffset.width -= xOffsetToBigBox
                bigBoxOffset.height += yOffsetToBigBox
              case .bottomRight:
                bigBoxOffset.width += xOffsetToBigBox
                bigBoxOffset.height += yOffsetToBigBox
            }
          }
        }
      }
        
      struct drawMBoxRectangleG4: View {
        let mediumBox:CGFloat = 128
        let lineWidth:CGFloat = 2
        
        @State var lineColor: Color
        
        var body: some View {
          Rectangle()
            .stroke(lineColor, lineWidth: lineWidth)
            .frame(width: mediumBox, height: mediumBox)
        }
      }

    var body: some View {
      ZStack {
        ZStack {
          drawMBoxRectangleG4(lineColor: Color.black)
        }
        
        ZStack {
          drawSBoxRectangleG4(lineColor: Color.red, boxDirection: .topLeft, bigBoxOffset: $bigBoxOffset)
        }.frame(width: bigBox, height: bigBox, alignment: .topLeading)
        
        ZStack {
          drawSBoxRectangleG4(lineColor: Color.blue, boxDirection: .topRight, bigBoxOffset: $bigBoxOffset)
        }.frame(width: bigBox, height: bigBox, alignment: .topTrailing)
        
        ZStack {
          drawSBoxRectangleG4(lineColor: Color.green, boxDirection: .bottomLeft, bigBoxOffset: $bigBoxOffset)
        }.frame(width: bigBox, height: bigBox, alignment: .bottomLeading)
        
        ZStack {
          drawSBoxRectangleG4(lineColor: Color.yellow, boxDirection: .bottomRight,  bigBoxOffset: $bigBoxOffset)
        }.frame(width: bigBox, height: bigBox, alignment: .bottomTrailing)
      }.offset(bigBoxOffset)
  }
}

struct SwiftUIViewG4_Previews: PreviewProvider {
    static var previews: some View {
      SwiftUIViewG4(shiftBox: .topLeft)
    }
}

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

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

import SwiftUI

struct SwiftUIViewG5: View {
  
  public enum boxMove {
    case topLeft
    case topRight
    case bottomLeft
    case bottomRight
  }
  
  public let shiftBox: boxMove
  private let bigBox:CGFloat = 256
  
  @State private var bigBoxOffset = CGSize(width: 0, height: 0)
  @State private var lineWidth:CGFloat = 2
  
  private struct drawSBoxRectangleG5: View {
    
    private let bigBox:CGFloat = 256
    private let smallBox:CGFloat = 32
    private let xOffsetToBigBox:CGFloat = 10
    private let yOffsetToBigBox:CGFloat = 10
    private let lineWidth:CGFloat = 2
    
    
    @State public var lineColor: Color
    @State public var boxDirection: boxMove
    @State private var alignToCorner: Alignment = .center
    
    @Binding public var bigBoxOffset: CGSize
    
    var body: some View {
      ZStack {
        Rectangle()
          .stroke(lineColor, lineWidth: lineWidth)
          .frame(width: smallBox, height: smallBox)
          .onTapGesture {
            switch boxDirection {
            case .topLeft:
              bigBoxOffset.width -= xOffsetToBigBox
              bigBoxOffset.height -= yOffsetToBigBox
            case .topRight:
              bigBoxOffset.width += xOffsetToBigBox
              bigBoxOffset.height -= yOffsetToBigBox
            case .bottomLeft:
              bigBoxOffset.width -= xOffsetToBigBox
              bigBoxOffset.height += yOffsetToBigBox
            case .bottomRight:
              bigBoxOffset.width += xOffsetToBigBox
              bigBoxOffset.height += yOffsetToBigBox
            }
          }.onAppear( perform: {
            switch boxDirection {
            case .topLeft:
              alignToCorner = .topLeading
            case .topRight:
              alignToCorner = .topTrailing
            case .bottomLeft:
              alignToCorner = .bottomLeading
            case .bottomRight:
              alignToCorner = .bottomTrailing
            }
          })
      }.frame(width: bigBox, height: bigBox, alignment: alignToCorner)
    }
  }
  
  private struct drawMBoxRectangleG5: View {
    
    private let mediumBox:CGFloat = 128
    private let lineWidth:CGFloat = 2
    
    @State public var lineColor: Color
    
    var body: some View {
      Rectangle()
        .stroke(lineColor, lineWidth: lineWidth)
        .frame(width: mediumBox, height: mediumBox)
    }
  }
  
  struct align: Hashable {
    var id = UUID()
    var directionToHead: boxMove
  }
  
  var corners:[align] = [align(directionToHead: .topLeft),
                         align(directionToHead: .topRight),
                         align(directionToHead: .bottomLeft),
                         align(directionToHead: .bottomRight)]
  
  var body: some View {
    ZStack {
      ZStack {
        drawMBoxRectangleG5(lineColor: Color.black)
      }
      
      ForEach(corners, id: \.self) { corner in
        drawSBoxRectangleG5(lineColor: Color.red, boxDirection: corner.directionToHead, bigBoxOffset: $bigBoxOffset)
      }
      
    }.offset(bigBoxOffset)
  }
}

struct SwiftUIViewG5_Previews: PreviewProvider {
  static var previews: some View {
    SwiftUIViewG5(shiftBox: .topLeft)
  }
}

سخن پایانی

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

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

اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.

اگر این مطلب برای شما مفید بوده است، آموزش‌ها و مطالب زیر نیز به شما پیشنهاد می‌شوند:

«میثم لطفی» دانش‌آموخته ریاضیات و شیفته فناوری به خصوص در حوزه رایانه است. وی در حال حاضر علاوه بر پیگیری علاقه‌مندی‌هایش در رشته‌های برنامه‌نویسی، کپی‌رایتینگ و محتوای چندرسانه‌ای، در زمینه نگارش مقالاتی با محوریت نرم‌افزار نیز با مجله فرادرس همکاری دارد.

آیا این مطلب برای شما مفید بود؟

نظر شما چیست؟

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