تكييف حل عملك الحالي مع SwiftUI. الجزء 2

يوم جيد للجميع! معك أنا ، أنا زاركوفا ، مطور جوال رائد في Usetech.
في هذا الجزء ، سنناقش بالفعل حالة كيفية تكييف حل جاهز لمشروع مع SwiftUI. إذا لم تكن على دراية خاصة بهذه التكنولوجيا ، أنصحك بأن تتعرف على مقدمة موجزة للموضوع .

لذا ، دعنا نلقي نظرة على مثال بسيط لكيفية استخدام مكتبة جاهزة لتطبيق iOS قياسي في تطبيق على SwiftUI.

لنأخذ الحل الكلاسيكي: تحميل الصور بشكل غير متزامن باستخدام مكتبة SDWebImage.



للراحة ، يتم تضمين العمل مع المكتبة في ImageManager ، والذي يستدعي:

  • SDWebImageDownloader
  • SDImageCache

لتنزيل الصور والتخزين المؤقت.

بالتقليد ، يتم تنفيذ التواصل مع نتيجة UIImageView المتلقية بطريقتين:

  • من خلال تمرير روابط ضعيفة إلى نفس UIImageView ؛
  • بتمرير كتلة الإغلاق إلى طريقة ImageManager


عادة ما يتم تغليف استدعاء ImageManager إما في امتداد UIImageView:

extension UIImageView {
 func setup(by key: String) {
        ImageManager.sharedInstance.setImage(toImageView: self, forKey: key)
    }
}

سواء في الفئة اللاحقة:

class CachedImageView : UIImageView {
    private var _imageUrl: String?
    var imageUrl: String?  {
        get {
            return _imageUrl
        }
        set {
            self._imageUrl = newValue
            if let url = newValue, !url.isEmpty {
                self.setup(by: url)
            }
        }
    }
    
    func setup(by key: String) {
        ImageManager.sharedInstance.setImage(toImageView: self, forKey: key)
    }
}

الآن دعنا نحاول تثبيت هذا الحل على SwiftUI. ومع ذلك ، عند التكيف ، يجب أن نأخذ في الاعتبار الميزات التالية للإطار:

- عرض - هيكل. الوراثة غير معتمد


- التمديد بالمعنى المعتاد عديم الفائدة. بالطبع ، يمكننا كتابة بعض الأساليب لتوسيع الوظيفة ، ولكننا بحاجة إلى ربط هذا بطريقة أو بأخرى بـ DataFlow ؛

لدينا مشكلة في الحصول على التعليقات والحاجة إلى تكييف منطق التفاعل بالكامل مع واجهة المستخدم مع DataDriven Flow.

بالنسبة للحل ، يمكننا الانتقال من جانب العرض ومن جانب التكيف مع تدفق البيانات.

لنبدأ مع العرض.

بادئ ذي بدء ، تذكر أن SwiftUI غير موجود بمفرده ، ولكن كإضافة إلى UIKit. قدم مطورو SwiftUI آلية للاستخدام في SwiftUI UIView ، وهي ليست من بين عناصر التحكم الجاهزة. لمثل هذه الحالات ، هناك بروتوكولات UIViewRepresentable و UIViewControllerRepresentable لتكييف UIView و UIViewController على التوالي.

إنشاء بنية عرض تطبق UIViewRpresentable ، حيث نعيد تعريف الأساليب:

  • جعل؛
  • تحديث

حيث نشير إلى UIView الذي نستخدمه ونضبط الإعدادات الأساسية. ولا تنسوا PropertyWrappers لخصائص قابلة للتغيير.

struct WrappedCachedImage : UIViewRepresentable {
    let height: CGFloat
    @State var imageUrl: String
    
    func makeUIView(context: Context) -> CachedImageView {
        let frame = CGRect(x: 20, y: 0, width: UIScreen.main.bounds.size.width - 40, 
                                     height: height)
        return CachedImageView(frame: frame)
    }
    
    func updateUIView(_ uiView: CachedImageView, context: Context) {
        uiView.imageUrl = imageUrl
        uiView.contentMode = .scaleToFill
    }
}

يمكننا تضمين عنصر التحكم الجديد الناتج في View SwiftUI:



هذا النهج له مزايا:

  • لا حاجة لتغيير عمل مكتبة موجودة
  • يتم تغليف المنطق في UIView المضمنة.

لكن هناك مسؤوليات جديدة. أولاً ، تحتاج إلى مراقبة إدارة الذاكرة في حزمة View-UIView. منذ هيكل العرض ، يتم تنفيذ كل العمل معهم في الخلفية بواسطة الإطار نفسه. لكن تنظيف الأشياء الجديدة يقع على أكتاف المطور.

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

على سبيل المثال ، لضبط الأحجام ، يمكننا استخدام GeometryReader بحيث تحتل صورتنا عرض الشاشة بالكامل والارتفاع الذي نحدده:

 var body: some View {
      GeometryReader { geometry in 
        VStack {
           WrappedCachedImage(height:300, imageUrl: imageUrl) 
           .frame(minWidth: 0, maxWidth: geometry.size.width,
                     minHeight: 0, maxHeight: 300)
        }
    }
}

من حيث المبدأ ، في مثل هذه الحالات ، يمكن اعتبار استخدام UIView المدمج الهندسة الزائدة . الآن دعونا نحاول حلها من خلال DataFlow SwiftUI.

يعتمد العرض الذي لدينا على متغير حالة أو مجموعة من المتغيرات ، أي من نموذج معين ، والذي يمكن أن يكون هو نفسه متغير الحالة هذا. في الأساس ، يعتمد هذا التفاعل على نمط MVVM.

نقوم بتنفيذ ما يلي:

  • إنشاء عرض مخصص ، والذي سنستخدم فيه تحكم SwiftUI ؛
  • إنشاء ViewModel ننقل منه منطق العمل مع Model (ImageManager).


لكي يكون هناك اتصال بين العرض و ViewModel ، يجب أن يقوم ViewModel بتطبيق بروتوكول ObservableObject والاتصال بـ View as a ObservedObject .

class CachedImageModel : ObservableObject {
    @Published var image: UIImage = UIImage()
    
    private var urlString: String = ""
    
    init(urlString:String) {
        self.urlString = urlString
    }
    
    func loadImage() {
        ImageManager.sharedInstance
        .receiveImage(forKey: urlString) {[weak self] (im) in
            guard let self = self else {return}
            DispatchQueue.main.async {
                self.image = im
            }
        }
    }
}

تستعرض طريقة onAppear لدورة حياتها طريقة ViewModel وتحصل على الصورة النهائية من خاصيةPublished:

struct CachedLoaderImage : View {
    @ObservedObject var  model:CachedImageModel
    
    init(withURL url:String) {
        self.model = CachedImageModel(urlString: url)
    }
    
    var body: some View {
        Image(uiImage: model.image)
            .resizable()
            .onAppear{
                self.model.loadImage()
        }
    }
    
}

هناك أيضًا واجهة برمجة تطبيقات موحّدة للعمل مع DataFlow SwiftUI . العمل معها يشبه إلى حد كبير العمل مع أطر العمل التفاعلية (نفس RxSwift): هناك مواضيع ، هناك مشتركون ، هناك طرق إدارة مماثلة ، هناك يمكن إلغاؤه (بدلاً من المتاح).

class ImageLoader: ObservableObject {
 @Published var image: UIImage?
 private var cancellable: AnyCancellable?

 func load(url: String) {
     cancellable = ImageManager.sharedInstance.publisher(for: url)
         .map { UIImage(data: $0.data) }
         .replaceError(with: nil)
         .receive(on: DispatchQueue.main)
         .assign(to: \.image, on: self)
 }

إذا تمت كتابة برنامج ImageManager الخاص بنا في الأصل باستخدام Combine ، فسيبدو الحل على هذا النحو.

لكن منذ يتم تنفيذ ImageManager مع مبادئ أخرى ، ثم سنحاول طريقة أخرى. لإنشاء حدث ، سنستخدم آلية PasstroughSubject ، التي تدعم الإكمال التلقائي للاشتراكات.

    var didChange = PassthroughSubject<UIImage, Never>()

سنرسل قيمة جديدة عند تعيين قيمة لخاصية UIImage لنموذجنا:

var data = UIImage() {
        didSet {
            didChange.send(data)
        }
    }
 ,    .

القيمة النهائية للعرض الخاص بنا "تستمع" في طريقة onReceive:

 var body: some View {
       Image(uiImage: image)
            .onReceive(imageLoader.didChange) { im in
            self.image = im
           //-   
        }
    }

لذا ، نظرنا إلى مثال بسيط لكيفية تكييف الشفرة الحالية مع SwiftUI.
ما تبقى للإضافة. إذا كان حل iOS الحالي يؤثر على جزء واجهة المستخدم أكثر ، فمن الأفضل استخدام التكيف من خلال UIViewRepresentable. في حالات أخرى ، هناك حاجة إلى التكيف من نموذج عرض الدولة.

في الأجزاء التالية ، سنلقي نظرة على كيفية تكييف منطق الأعمال لمشروع حالي مع SwiftUI ، والعمل مع التنقل ، ثم البحث في التكيف للدمج بشكل أعمق.

لمزيد من المعلومات حول العمل مع عرض تحت SwiftUI ، انظر هنا .

All Articles