SwiftUI على الرفوف: الرسوم المتحركة. الجزء الأول

صورة

لقد صادفت مؤخرًا مقالًا جديدًا حاول فيه الرجال إعادة إنتاج مفهوم مثير للاهتمام باستخدام SwiftUI. إليك ما فعلوه:

صورة

لقد درست رمزهم باهتمام ، لكنني شعرت ببعض الإحباط. لا ، ليس بمعنى أنهم ارتكبوا شيئًا خاطئًا ، على الإطلاق. لم أتعلم من شيفرتهم عمليًا أي شيء جديد. إن تنفيذها يتعلق بالدمج أكثر من الرسوم المتحركة. وقررت بناء lunopark الخاص بي لكتابة مقالتي عن الرسوم المتحركة في SwiftUI ، وتنفيذ نفس المفهوم ، ولكن باستخدام 100 ٪ من قدرات الرسوم المتحركة المضمنة ، حتى لو لم تكن فعالة للغاية. للدراسة - حتى النهاية. للتجربة - حتى مع وميض :)

إليك ما حصلت عليه:


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

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

تحذير: تحت القطة هناك الكثير من الصور والرسوم المتحركة gif.

TLDR


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

في هذه المقالة ، سوف نناقش الأساسيات فقط ، ونؤثر فقط على محتويات مجلد القواعد.

المقدمة


أعترف أنني لن أكتب هذه المقالة الآن. كان لدي خطة يفترض بموجبها أن مقالًا عن الرسوم المتحركة هو الثالث أو الرابع على التوالي. ومع ذلك ، لم أستطع المقاومة ، أردت حقًا تقديم وجهة نظر بديلة.

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

خياري بالتأكيد ليس أكثر أناقة وأسرع وأسهل في الصيانة. ومع ذلك ، فإنه يكشف بشكل أفضل بكثير ما هو تحت غطاء SwiftUI ، وفي الواقع كان هذا هو الغرض من المقالة - لمعرفة ذلك أولاً.

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

نحن 1c-nicks مررنا بهذا مع "أشكال خاضعة للرقابة". هذا هو نوع من SwiftUI في عالم 1s ، والذي حدث قبل أكثر من 10 سنوات. في الواقع ، فإن القياس دقيق جدًا ، لأن النماذج المُدارة ليست سوى طريقة جديدة لرسم واجهة. ومع ذلك ، فقد غيّر تمامًا تفاعل العميل والخادم للتطبيق ككل ، وصورة العالم في أذهان المطورين على وجه الخصوص. لم يكن هذا سهلاً ، أنا نفسي لم أرغب في دراسته لمدة 5 سنوات ، لأنه اعتقدت أن العديد من الفرص التي قطعت هناك كانت ضرورية ببساطة بالنسبة لي. ولكن ، كما أظهرت الممارسة ، فإن الترميز في النماذج المدارة ليس ممكنًا فحسب ، بل ضروريًا فقط.

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

شكل متحرك


كيف تعمل الرسوم المتحركة بشكل عام


لذا ، فإن الفكرة الرئيسية للرسوم المتحركة هي تحويل تغيير معين منفصل إلى عملية مستمرة. على سبيل المثال ، كان نصف قطر الدائرة 100 وحدة ، وأصبح 50 وحدة. بدون الرسوم المتحركة ، سيحدث التغيير على الفور ، مع الرسوم المتحركة - بسلاسة. كيف تعمل؟ بسيط جدا. لإجراء تغييرات سلسة ، نحتاج إلى استيفاء العديد من القيم ضمن قسم "لقد كان ... لقد أصبح". في حالة نصف القطر ، سيتعين علينا رسم عدة دوائر متوسطة يبلغ نصف قطرها 98 وحدة و 95 وحدة و 90 وحدة ... 53 وحدة ، وأخيرًا 50 وحدة. يمكن لـ SwiftUI القيام بذلك بسهولة وبشكل طبيعي ، ما عليك سوى لف الكود الذي يقوم بهذا التغيير في withAnimation {...}. يبدو الأمر سحريًا ... حتى تريد تنفيذ شيء أكثر تعقيدًا من "مرحبًا بالعالم".

دعنا ننتقل إلى الأمثلة. يعتبر أبسط الكائنات وأكثرها قابلية للفهم للرسوم المتحركة رسومًا متحركة للأشكال. الشكل (سأستمر في استدعاء البنية المطابقة لبروتوكول شكل الشكل) في SwiftUI عبارة عن بنية ذات معلمات يمكن أن تتناسب مع هذه الحدود. أولئك. وهي بنية لها جسم الوظيفة (في المستقيم: CGRect) -> Path. كل وقت التشغيل المطلوب لرسم هذا النموذج هو طلب مخططه التفصيلي (نتيجة الوظيفة هي كائن من النوع Path ، في الواقع ، هو منحنى Bezier) للحجم المطلوب (المحدد كمعلمة دالة ، مستطيل من نوع CGRect).

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

لنبدأ الترميز بالفعل:

struct CircleView: View{
    var radius: CGFloat
    var body: some View{
        Circle()
            .fill(Color.green)
            .frame(height: self.radius * 2)
            .overlay(
                Text("Habra")
                    .font(.largeTitle)
                    .foregroundColor(.gray)
                )

    }
}
struct CustomCircleView: View{
    var radius: CGFloat
    var body: some View{
        CustomCircle()
            .fill(Color.gray)
            .frame(width: self.radius * 2, height: self.radius * 2)
            .overlay(
                Text("Habr")
                    .font(.largeTitle)
                    .foregroundColor(.green)
                )
    }
}
struct CustomCircleTestView: View {
    @State var radius: CGFloat = 50
    var body: some View {
        VStack{
            CircleView(radius: radius)
               .frame(height: 200)
            Slider(value: self.$radius, in: 42...100)
            Button(action: {
                withAnimation(.linear(duration: 1)){
                    self.radius = 50
                }
            }){
                Text("set default radius")
            }
        }
    }
}


لذا ، لدينا دائرة (Circle ()) ، يمكن تغيير نصف قطرها باستخدام شريط التمرير. يحدث هذا بسلاسة يعطينا شريط التمرير جميع القيم الوسيطة. ومع ذلك ، عند النقر فوق الزر "تعيين نصف القطر الافتراضي" ، لا يحدث التغيير أيضًا على الفور ، ولكن وفقًا لتعليمات withAnimation (. الخطية (المدة: 1)). خطيا ، دون تسارع ، امتدت لمدة ثانية واحدة. صف دراسي! نحن نتقن الرسوم المتحركة! نحن نختلف :)

ولكن ماذا لو أردنا تطبيق نموذجنا وتحريك تغييراته؟ هل من الصعب القيام بذلك؟ دعونا تحقق.

قمت بعمل نسخة من الدائرة على النحو التالي:

struct CustomCircle: Shape{
    public func path(in rect: CGRect) -> Path{
        let radius = min(rect.width, rect.height) / 2
        let center = CGPoint(x: rect.width / 2, y: rect.height / 2)
        return Path(){path in
            if rect.width > rect.height{
                path.move(to: CGPoint(x: center.x, y: 0))
                let startAngle = Angle(degrees: 270)
                path.addArc(center: center, radius: radius, startAngle: startAngle, endAngle:  startAngle + Angle(degrees: 360), clockwise: false)
            }else{
                path.move(to: CGPoint(x: 0, y: center.y))
                let startAngle = Angle(degrees: 0)
                path.addArc(center: center, radius: radius, startAngle: startAngle, endAngle:  startAngle + Angle(degrees: 360), clockwise: false)
            }
            path.closeSubpath()
        }
    }
}

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

الملاحظة 1
Apple ( ) . , (0, 0), (x, y), x — , y — . .. y. y — . , . 90 , 180 — , 270 — .

ملاحظة 2
1 , “ ” “ ” . Core Graphics (SwiftUI ):
In a flipped coordinate system (the default for UIView drawing methods in iOS), specifying a clockwise arc results in a counterclockwise arc after the transformation is applied.

دعونا نتحقق من كيفية رد دائرتنا الجديدة على التغييرات في كتلة withAnimation:

struct CustomCircleView: View{
    var radius: CGFloat
    var body: some View{
        CustomCircle()
            .fill(Color.gray)
            .frame(width: self.radius * 2, height: self.radius * 2)
            .overlay(
                Text("Habr")
                    .font(.largeTitle)
                    .foregroundColor(.green)
                )
    }
}

struct CustomCircleTestView: View {
    @State var radius: CGFloat = 50
    var body: some View {
        VStack{
                HStack{
                CircleView(radius: radius)
                    .frame(height: 200)
                CustomCircleView(radius: radius)
                    .frame(height: 200)
            }
            Slider(value: self.$radius, in: 42...100)
            Button(action: {
                withAnimation(.linear(duration: 1)){
                    self.radius = 50
                }
            }){
                Text("set default radius")
            }
        }
    }
}


رائع! لقد تعلمنا كيفية صنع صورنا المجانية وتحريكها! إنه كذلك؟

ليس صحيحا. يتم تنفيذ جميع الأعمال هنا بواسطة مُعدل الإطار. (العرض: self.radius * 2 ، الارتفاع: self.radius * 2). داخل كتلة withAnimation {...} نتغيرحالةمتغير ، يرسل إشارة لإعادة تهيئة CustomCircleView () بقيمة نصف قطر جديدة ، تقع هذه القيمة الجديدة في معدل .frame () ، ويمكن لهذا المعدل بالفعل تحريك تغييرات المعلمة. يتفاعل نموذج CustomCircle () الخاص بنا مع هذا مع الرسوم المتحركة ، لأنه لا يعتمد على أي شيء بخلاف حجم المنطقة المحددة له. يحدث تغيير المساحة مع الرسم المتحرك ، (أي تدريجياً ، حيث أصبح إقحام القيم الوسيطة بينه) ، وبالتالي يتم رسم دائرتنا بنفس الرسوم المتحركة.

دعونا نبسط (أو لا تزال معقدة؟) شكلنا قليلا. لن نحسب نصف القطر بناءً على حجم المساحة المتاحة ، لكننا سننقل نصف القطر في الشكل النهائي ، أي جعلها معلمة بنية مخزنة.

struct CustomCircle: Shape{
    var radius: CGFloat
    public func path(in rect: CGRect) -> Path{
        //let radius = min(rect.width, rect.height) / 2
...
    }
}

struct CustomCircleView: View{
    var radius: CGFloat
    var body: some View{
        CustomCircle(radius: radius)
            .fill(Color.gray)
            //.frame(height: self.radius * 2)
...
    }
}


حسنًا ، لقد ضاع السحر بلا رجعة.

لقد استبعدنا مُعدِّل الإطار () من CustomCircleView () الخاص بنا ، مما نقل المسؤولية عن حجم الدائرة إلى الشكل نفسه ، واختفت الرسوم المتحركة. لكن هذا لا يهم ؛ إن تعليم شكل لتحريك التغييرات في معلماته ليس صعبًا للغاية. للقيام بذلك ، تحتاج إلى تنفيذ متطلبات بروتوكول Animatable:

struct CustomCircle: Shape, Animatable{
    var animatableData: CGFloat{
         get{
             radius
         }
         set{
            print("new radius is \(newValue)")
            radius = newValue
         }
     }
    var radius: CGFloat
    public func path(in rect: CGRect) -> Path{
	...
}


هاهو! لقد عاد السحر مرة أخرى!

والآن يمكننا أن نقول بثقة أن شكلنا متحرك حقًا - يمكن أن يعكس التغييرات في معلماته مع الرسوم المتحركة. أعطينا النظام نافذة حيث يمكنه أن يحشر القيم المحرفه اللازمة للرسوم المتحركة. إذا كان هناك مثل هذه النافذة ، فإن التغييرات تكون متحركة. إذا لم يكن الأمر كذلك ، فستحدث التغييرات بدون رسوم متحركة ، أي فورا. لا شيء معقد ، أليس كذلك؟

AnimatableModifier


كيفية تحريك التغييرات داخل العرض


ولكن دعنا نذهب مباشرة للعرض. لنفترض أننا نريد تحريك موضع عنصر داخل الحاوية. في حالتنا ، سيكون مستطيلًا بسيطًا باللون الأخضر وعرض 10 وحدات. سنقوم بتحريك موقعه الأفقي.

struct SimpleView: View{
    @State var position: CGFloat = 0
    var body: some View{
        VStack{
            ZStack{
                Rectangle()
                    .fill(Color.gray)
                BorderView(position: position)
            }
            Slider(value: self.$position, in: 0...1)
            Button(action: {
                withAnimation(.linear(duration: 1)){
                    self.position = 0
                }
            }){
                Text("set to 0")
            }
        }
    }
}

struct BorderView: View,  Animatable{
    public var animatableData: CGFloat {
        get {
            print("Reding position: \(position)")
            return self.position
        }
        set {
            self.position = newValue
            print("setting position: \(position)")
        }
    }
    let borderWidth: CGFloat
    init(position: CGFloat, borderWidth: CGFloat = 10){
        self.position = position
        self.borderWidth = borderWidth
        print("BorderView init")
    }
    var position: CGFloat
    var body: some View{
        GeometryReader{geometry in
            Rectangle()
                .fill(Color.green)
                .frame(width: self.borderWidth)
                .offset(x: self.getXOffset(inSize: geometry.size), y: 0)
                // .borderIn(position: position)
        }
    }
    func getXOffset(inSize: CGSize) -> CGFloat{
        print("calculating position: \(position)")
        return -inSize.width / 2 + inSize.width * position
    }
}


صف دراسي! يعمل! الآن نعرف كل شيء عن الرسوم المتحركة!

ليس صحيحا. إذا نظرت إلى وحدة التحكم ، فسوف نرى ما يلي:
موضع
حساب BorderView init : 0.4595176577568054 موضع حساب BorderView init : 0.468130886554718
موقع
حساب
BorderView init
: 0.0

أولاً ، يؤدي كل تغيير في قيمة الموضع باستخدام شريط التمرير إلى إعادة تكوين BorderView بالقيمة الجديدة. هذا هو السبب في أننا نرى حركة سلسة للخط الأخضر بعد شريط التمرير ، حيث يشير شريط التمرير ببساطة في كثير من الأحيان إلى تغيير في المتغير ، ويبدو كرسوم متحركة ، ولكنه ليس كذلك. يعد استخدام شريط التمرير ملائمًا حقًا عند تصحيح الرسوم المتحركة. يمكنك استخدامه لتتبع بعض الحالات الانتقالية.

ثانيًا ، نرى أن موضع الحساب أصبح يساوي ببساطة 0 ، ولا توجد سجلات وسيطة ، كما كان الحال مع الرسوم المتحركة الصحيحة للدائرة. لماذا ا؟

الشيء ، كما في المثال السابق ، في المعدل. هذه المرة ، يحصل معدّل .offset () على قيمة المسافة البادئة الجديدة ، ويقوم بتحريك التغيير نفسه. أولئك. في الواقع ، لم يكن التغيير في معلمة الموضع هو ما أردنا تحريكه ، ولكن التغيير الأفقي للمسافة البادئة في معدّل .offset () المستمد منه. في هذه الحالة ، هذا بديل غير ضار ، والنتيجة هي نفسها. ولكن منذ قدومهم ، دعونا نتعمق أكثر. دعونا نجعل معدّلنا الخاص ، والذي سيتلقى الموضع (من 0 إلى 1) عند الإدخال ، سيتلقى حجم المساحة المتاحة وحساب المسافة البادئة.

struct BorderPosition: ViewModifier{
    var position: CGFloat
    func body(content: Content) -> some View {
        GeometryReader{geometry in
            content
            .offset(x: self.getXOffset(inSize: geometry.size), y: 0)
            .animation(nil)
        }
    }
    func getXOffset(inSize: CGSize) -> CGFloat{
        let offset = -inSize.width / 2 + inSize.width * position
        print("at position  \(position) offset is \(offset)")
        return offset
    }
}

extension View{
    func borderIn(position: CGFloat) -> some View{
        self.modifier(BorderPosition(position: position))
    }
}

في BorderView الأصلي ، على التوالي ، لم تعد هناك حاجة إلى GeometryReader ، بالإضافة إلى وظيفة حساب المسافة البادئة:

struct BorderView: View,  Animatable{
    ...
    var body: some View{
            Rectangle()
                .fill(Color.green)
                .frame(width: self.borderWidth)
                .borderIn(position: position)
    }
}


نعم ، ما زلنا نستخدم مُعدل .offset () داخل المُعدِّل الخاص بنا ، ولكن بعد ذلك أضفنا مُعدل .imimation (لا شيء) ، الذي يمنع الرسوم المتحركة الخاصة بالإزاحة. أفهم أنه في هذه المرحلة يمكنك أن تقرر أنه يكفي إزالة هذا القفل ، ولكن بعد ذلك لن نصل إلى الحقيقة. والحقيقة هي أن خدعتنا مع animatableData لـ BorderView لا تعمل. في الواقع ، إذا نظرت إلى وثائق بروتوكول Animatable ، فستلاحظ أن تطبيق هذا البروتوكول مدعوم فقط لـ AnimatableModifier و GeometryEffect و Shape. المنظر ليس بينهم.

النهج الصحيح لتحريك التعديلات


كان النهج نفسه ، عندما نطلب من View تحريك بعض التغييرات ، غير صحيح. للعرض ، لا يمكنك استخدام نفس النهج كما في النماذج. بدلاً من ذلك ، يجب تضمين الرسوم المتحركة في كل معدل. تدعم معظم المعدِّلات المضمنة الرسوم المتحركة خارج الصندوق. إذا كنت تريد رسومًا متحركة لمعدِّلاتك الخاصة ، فيمكنك استخدام بروتوكول AnimatableModifier بدلاً من ViewModifier. وهناك يمكنك تنفيذ نفس الشيء عند تغيير شكل الرسوم المتحركة ، كما فعلنا أعلاه.

struct BorderPosition: AnimatableModifier {
    var position: CGFloat
    let startDate: Date = Date()
    public var animatableData: CGFloat {
        get {
            print("reading position: \(position) at time \(Date().timeIntervalSince(startDate))")
            return position
        }
        set {
            position = newValue
            print("setting position: \(position) at time \(Date().timeIntervalSince(startDate))")
        }
    }
    func body(content: Content) -> some View {
...
    }
    ...
}


الآن كل شيء على ما يرام. تساعد الرسائل الموجودة في وحدة التحكم على فهم أن الرسوم المتحركة تعمل بالفعل ، وأن الرسوم المتحركة (لا شيء) داخل المعدل لا تتداخل معها على الإطلاق. ولكن دعونا لا نزال نعرف كيف يعمل بالضبط.

أولاً ، تحتاج إلى فهم ماهية المُعدِّل.


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

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

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

بعض المقارنات البدائية


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

وظيفة .modifier (BorderPosition (position: position)) ، التي حولنا بها هيكل BorderPosition إلى معدل ، تضع فقط برغيًا إضافيًا في الدرج على ساق الطاولة. هيكل BorderPosition هو هذا المسمار. يأخذ العرض ، في وقت العرض ، هذا المربع ، ويخرج منه ساقًا (Rectangle () في حالتنا) ، ويحصل بالتسلسل على جميع المعدلات من القائمة ، مع القيم المخزنة فيها. وظيفة الجسم لكل معدّل هي تعليمات حول كيفية ربط ساق على سطح الطاولة باستخدام هذا المسمار ، والهيكل نفسه مع الخصائص المخزنة ، هذا هو ذلك المسمار.

لماذا من المهم أن نفهم هذا في سياق الرسوم المتحركة؟ لأن الرسم المتحرك يسمح لك بتغيير معلمات أحد المعدلات دون التأثير على الآخرين ، ثم إعادة عرض الصورة. إذا فعلت الشيء نفسه بتغيير بعض@Stateالمعلمات - سيؤدي ذلك إلى إعادة تهيئة العرض المتداخل وهياكل التعديل وما إلى ذلك ، على طول السلسلة. لكن الرسوم المتحركة ليست كذلك.

في الواقع ، عندما نغير قيمة الموضع عندما نضغط على زر ، يتغير. الحق في النهاية. لا يتم تخزين حالات وسيطة في المتغير نفسه ، والذي لا يمكن قوله عن المُعدِّل. لكل إطار جديد ، تتغير قيم معلمات التعديل وفقًا لتقدم الرسوم المتحركة الحالية. إذا استمرت الرسوم المتحركة لمدة ثانية واحدة ، فإن كل 1/60 من الثانية (يُظهر جهاز iPhone هذا العدد من الإطارات بالضبط في الثانية) ، ستتغير قيمة البيانات المتحركة داخل المُعدِّل ، ثم سيتم قراءتها بواسطة العرض للعرض ، وبعد ذلك ، بعد 1/60 أخرى من الثانية ستكون تغير مرة أخرى ، وقراءة مرة أخرى من قبل التقديم.

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

الفرق =

الصلب
- كان القيمة الحالية = الصلب - الفرق * (1 - الوقت المنقضي) الوقت المنقضي = الوقت من StartAnimations / DurationAnimations

يجب حساب القيمة الحالية عدة مرات مثل عدد الإطارات التي نحتاج إلى إظهارها.

لماذا لم يتم استخدام "هل" صراحة؟ والحقيقة هي أن SwiftUI لا يخزن الحالة الأولية. يتم تخزين الفرق فقط: لذا ، في حالة حدوث نوع من الفشل ، يمكنك ببساطة إيقاف الرسوم المتحركة ، والانتقال إلى الحالة الحالية "تصبح".

يتيح لك هذا النهج جعل الرسوم المتحركة قابلة للعكس. لنفترض ، في مكان ما في منتصف رسم متحرك ، ضغط المستخدم على زر مرة أخرى ، وقمنا مرة أخرى بتغيير قيمة نفس المتغير. في هذه الحالة ، كل ما نحتاج إلى فعله للتغلب على هذا التغيير بشكل جميل هو أخذ "التيار" داخل الرسوم المتحركة في وقت التغيير الجديد باسم "It" ، وتذكر الفرق الجديد ، وبدء رسم متحرك جديد بناءً على "Became" الجديد و "الفرق" الجديد . نعم ، في الواقع ، قد تكون هذه التحولات من رسم متحرك إلى آخر أكثر صعوبة في محاكاة القصور الذاتي ، ولكن أعتقد أن المعنى مفهوم.

المثير للاهتمام هو أن الرسوم المتحركة التي يطلبها كل إطار للقيمة الحالية داخل المُعدِّل (باستخدام المُحضِّر). هذا ، كما ترون من سجلات الخدمة في السجل ، مسؤول عن حالة "الفولاذ". ثم ، باستخدام أداة الضبط ، نقوم بتعيين الحالة الجديدة الحالية لهذا الإطار. بعد ذلك ، بالنسبة للإطار التالي ، يتم طلب القيمة الحالية من المُعدِّل مرة أخرى - ومرة ​​أخرى "أصبحت" ، أي القيمة النهائية التي تتحرك الرسوم المتحركة إليها. من المحتمل أن يتم استخدام نُسخ من هياكل التعديل في الرسوم المتحركة ، ويتم استخدام بادئ بنية (معدِّل حقيقي للعرض الفعلي) للحصول على قيمة "الفولاذ" ، ومضبط لآخر (معدِّل مؤقت يستخدم للرسوم المتحركة). لم أتوصل إلى طريقة للتأكد من ذلك ، ولكن من خلال المؤشرات غير المباشرة يبدو كل شيء على هذا النحو. على أي حال،لا تؤثر التغييرات داخل الرسم المتحرك على القيمة المخزنة لهيكل التعديل في العرض الحالي. إذا كانت لديك أفكار حول كيفية معرفة بالضبط ما يحدث بالضبط مع المُدرب والمُحضر ، فاكتب عنه في التعليقات ، وسأقوم بتحديث المقالة.

العديد من المعلمات


حتى هذه اللحظة ، كان لدينا معلمة واحدة فقط للرسوم المتحركة. قد يطرح السؤال ، ولكن ماذا عن إذا تم تمرير أكثر من معلمة إلى المُعدِّل؟ وإذا كان كلاهما بحاجة إلى الرسوم المتحركة في نفس الوقت؟ إليك كيفية تعديل الإطار (العرض: الارتفاع :) على سبيل المثال. بعد كل شيء ، يمكننا تغيير عرض وارتفاع هذا العرض في وقت واحد ، ونريد أن يحدث التغيير في رسم متحرك واحد ، كيف نفعل ذلك؟ بعد كل شيء ، المعلمة AnimatableData هي واحدة ، ما هو البديل؟

إذا نظرت ، لدى Apple متطلبات واحدة فقط للبيانات المتحركة. يجب أن يلبي نوع البيانات الذي تستبدله فيه بروتوكول VectorArithmetic. يتطلب هذا البروتوكول الكائن لضمان الحد الأدنى من العمليات الحسابية اللازمة لتكون قادرة على تشكيل قطعة من قيمتين ، واستيفاء النقاط داخل هذا المقطع. العمليات اللازمة لذلك هي الجمع والطرح والضرب. تكمن الصعوبة في أنه يجب علينا تنفيذ هذه العمليات باستخدام كائن واحد يخزن العديد من المعلمات. أولئك. يجب أن نحزم القائمة الكاملة لمعلماتنا في حاوية ستكون متجهًا. توفر Apple مثل هذا الشيء من خارج الصندوق ، وتعرض علينا استخدام حل جاهز للاستخدام في الحالات غير الصعبة للغاية. يطلق عليه AnimatablePair.

دعنا نغير المهمة قليلاً. نحتاج إلى معدِّل جديد لن يحرك الشريط الأخضر فحسب ، بل سيغير ارتفاعه أيضًا. ستكون هذه معلمتين معدلتين مستقلتين. لن أعطي الشفرة الكاملة لجميع التغييرات التي يجب القيام بها ، يمكنك رؤيتها على github في ملف SimpleBorderMove. سأظهر فقط المعدل نفسه:

struct TwoParameterBorder: AnimatableModifier {
    var position: CGFloat
    var height: CGFloat
    let startDate: Date = Date()
    public var animatableData: AnimatablePair<CGFloat, CGFloat> {
        get {
           print("animation read position: \(position), height: \(height)")
           return AnimatablePair(position, height)
        }
        set {
            self.position = newValue.first
            print("animating position at \(position)")
            self.height = newValue.second
            print("animating height at \(height)")
        }
    }
    init(position: CGFloat, height: CGFloat){
        self.position = position
        self.height = height
    }
    func body(content: Content) -> some View {
        GeometryReader{geometry in
            content
                .animation(nil)
                .offset(x: -geometry.size.width / 2 + geometry.size.width * self.position, y: 0)
                .frame(height: self.height * (geometry.size.height - 20) + 20)
        }
    }
}


أضفت منزلقًا آخر وزرًا لتغيير المعلمتين بشكل عشوائي دفعة واحدة في طريقة العرض الرئيسية لـ SimpleView ، ولكن لا يوجد شيء مثير للاهتمام ، لذلك بالنسبة إلى الشفرة الكاملة ، مرحبًا بك في github.

كل شيء يعمل ، نحصل حقًا على تغيير ثابت في زوج المعلمات المعبأة في مجموعة AnimatablePair. ليس سيئا.

لا شيء يخلط في هذا التنفيذ؟ أنا شخصياً متوترة عندما رأيت هذا التصميم:

        
self.position = newValue.first
self.height = newValue.second

لم أذكر في أي مكان أي من هذه المعلمات يجب أن تذهب أولاً وأيها. كيف يقرر SwiftUI أي قيمة يتم وضعها أولاً وأي قيمة في الثانية؟ حسنًا ، ألا تتطابق مع أسماء معلمات الوظيفة مع أسماء سمات البنية؟

كانت الفكرة الأولى هي ترتيب السمات في معلمات الدالة وأنواعها ، كما يحدث معEnvironmentObject. هناك ، نقوم ببساطة بوضع القيم في المربع ، دون تعيين أي تصنيفات لها ، ثم نخرجها من هناك ، أيضًا دون الإشارة إلى أي تصنيفات. هناك ، اكتب المسائل ، وضمن نوع واحد ، ترتيب. وبأي ترتيب يضعونه في الصندوق ، بنفس الطريقة ويحصلون عليه. لقد جربت ترتيبًا مختلفًا لحجج الوظيفة ، ترتيب الحجج لتهيئة الهيكل ، ترتيب سمات الهيكل نفسه ، ضار رأسي عمومًا بالحائط ، لكن لم أستطع الخلط بين SwiftUI حتى بدأ في تحريك الموقف بقيم الارتفاع والعكس بالعكس.

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

ولكن دعونا نتحقق من ذلك. بعد كل شيء ، يمكننا إنشاء ناقل الحاوية الخاص بنا (أوه ، أنا أحبه ، إنشاء تطبيقنا الخاص للكائنات الموجودة ، ربما لاحظت ذلك من مقال سابق).



نحن نصف الهيكل الأساسي ، ونعلن دعم بروتوكول VectorArithmetic ، ونفتح الخطأ حول البروتوكول غير المطابق ، وانقر فوق إصلاح ، ونحصل على التصريح عن جميع الوظائف المطلوبة والمعلمات المحسوبة. يبقى فقط لملئها.

بنفس الطريقة ، نملأ كائننا بالطرق المطلوبة لبروتوكول AdditiveArithmetic (يتضمن VectorArithmetic دعمه).

struct MyAnimatableVector: VectorArithmetic{
    static func - (lhs: MyAnimatableVector, rhs: MyAnimatableVector) -> MyAnimatableVector {
        MyAnimatableVector(position: lhs.position - rhs.position, height: lhs.height - rhs.height)
    }
    
    static func + (lhs: MyAnimatableVector, rhs: MyAnimatableVector) -> MyAnimatableVector {
        MyAnimatableVector(position: lhs.position + rhs.position, height: lhs.height + rhs.height)
    }
    
    mutating func scale(by rhs: Double) {
        self.position = self.position * CGFloat(rhs)
        self.height = self.height * CGFloat(rhs)
    }
    
    var magnitudeSquared: Double{
         Double(self.position * self.position) + Double(self.height * self.height)
    }
    
    static var zero: MyAnimatableVector{
        MyAnimatableVector(position: 0, height: 0)
    }
    
    var position: CGFloat
    var height: CGFloat
}

  • أعتقد لماذا نحتاج + و - من الواضح.
  • المقياس هو وظيفة التحجيم. نأخذ الفرق "لقد كان - لقد أصبح" ونضربه في المرحلة الحالية من الرسوم المتحركة (من 0 إلى 1). "أصبح + اختلاف * (1 - المرحلة)" وستكون هناك قيمة حالية يجب أن نحصل عليها في بيانات متحركة
  • ربما تكون هناك حاجة إلى الصفر لتهيئة الكائنات الجديدة التي سيتم استخدام قيمها للرسوم المتحركة. تستخدم الرسوم المتحركة .zero في البداية ، ولكن لم أستطع معرفة كيف بالضبط. ومع ذلك ، لا أعتقد أن هذا مهم.
  • المقدار المربّع هو نتاج عددي لناقل معين في حد ذاته. بالنسبة للفضاء ثنائي الأبعاد ، هذا يعني طول مربع المتجه. ربما يستخدم هذا ليكون قادرًا على مقارنة كائنين مع بعضهما البعض ، ليس بشكل عنصري ، ولكن ككل. يبدو أنه لا يستخدم لأغراض الرسوم المتحركة.

بشكل عام ، يتم تضمين وظائف "- =" "+ =" أيضًا في دعم البروتوكول ، ولكن بالنسبة للهيكل ، يمكن إنشاؤها تلقائيًا في هذا النموذج

    static func -= (lhs: inout MyAnimatableVector, rhs: MyAnimatableVector) {
        lhs = lhs - rhs
    }

    static func += (lhs: inout MyAnimatableVector, rhs: MyAnimatableVector) {
        lhs = lhs + rhs
    }


من أجل الوضوح ، حددت كل هذا المنطق في شكل رسم بياني. الصورة قابلة للنقر. يتم تمييز ما نحصل عليه أثناء الرسوم المتحركة باللون الأحمر - كل علامة تالية (1/60 ثانية) يعطي الموقت قيمة جديدة لـ t ، ونحن في أداة التعديل لدينا نحصل على قيمة جديدة للبيانات المتحركة. هكذا تعمل الرسوم المتحركة تحت غطاء المحرك. في الوقت نفسه ، من المهم أن نفهم أن المُعدِّل هو بنية مُخزنة ، وأن نسخة من المُعدِّل الحالي بحالة جديدة تُستخدم لعرض الرسوم المتحركة.





لماذا يمكن أن تكون AnimatableData مجرد هيكل


هناك نقطة أخرى. لا يمكنك استخدام الفئات ككائن AnimatableData. رسميًا ، يمكنك أن تصف للفصل جميع الطرق الضرورية للبروتوكول المقابل ، ولكن هذا لن ينطلق ، وإليك السبب. كما تعلم ، فإن الفئة هي نوع بيانات مرجعي ، والبنية هي نوع بيانات قائم على القيمة. عندما تقوم بإنشاء متغير واحد على أساس آخر ، في حالة فئة ، تقوم بنسخ ارتباط إلى هذا الكائن ، وفي حالة بنية ، تقوم بإنشاء كائن جديد بناءً على قيم الموجودة. هنا مثال صغير يوضح هذا الاختلاف:

    struct TestStruct{
        var value: CGFloat
        mutating func scaled(by: CGFloat){
            self.value = self.value * by
        }
    }
    class TestClass{
        var value: CGFloat
        func scaled(by: CGFloat){
             self.value = self.value * by
        }
        init(value: CGFloat){
            self.value = value
        }
    }
        var stA = TestStruct(value: 5)
        var stB = stA
        stB.scaled(by: 2)
        print("structs: a = \(stA.value), b = \(stB.value))") //structs: a = 5.0, b = 10.0)
        var clA = TestClass(value: 5)
        var clB = clA
        clB.scaled(by: 2)
        print("classes: a = \(clA.value), b = \(clB.value))") //classes: a = 10.0, b = 10.0)

مع الرسوم المتحركة يحدث نفس الشيء بالضبط. لدينا كائن AnimatableData يمثل الفرق بين "كان" و "أصبح". نحتاج إلى حساب جزء من هذا الاختلاف للتأمل على الشاشة. للقيام بذلك ، يجب علينا نسخ هذا الاختلاف وضربه برقم يمثل المرحلة الحالية من الرسوم المتحركة. في حالة الهيكل ، لن يؤثر هذا على الاختلاف نفسه ، ولكن في حالة الطبقة سوف يؤثر. الإطار الأول الذي نرسمه هو الدولة "was". للقيام بذلك ، نحن بحاجة إلى حساب الصلب + الفرق * المرحلة الحالية - الفرق. في حالة الفئة ، في الإطار الأول نضرب الفرق في 0 ، ونصفره ، ويتم رسم جميع الإطارات اللاحقة بحيث يكون الفرق = 0. أي يبدو أن الرسوم المتحركة تم رسمها بشكل صحيح ، ولكن في الواقع نرى انتقالًا فوريًا من حالة إلى أخرى ، كما لو لم يكن هناك رسوم متحركة.

ربما يمكنك كتابة نوع من التعليمات البرمجية ذات المستوى المنخفض التي تنشئ عناوين ذاكرة جديدة لنتائج الضرب - ولكن لماذا؟ يمكنك فقط استخدام الهياكل - يتم إنشاؤها لذلك.

بالنسبة لأولئك الذين يريدون أن يفهموا تمامًا كيف يحسب SwiftUI القيم الوسيطة ، وبأي عمليات وفي أي لحظة ، يتم دفع الرسائل إلى وحدة التحكم في المشروع . بالإضافة إلى ذلك ، أدرجت السكون 0.1 ثانية هناك لمحاكاة الحسابات كثيفة الموارد داخل الرسوم المتحركة ، واستمتع :)

الرسوم المتحركة للشاشة: .transition ()


حتى هذه اللحظة ، تحدثنا عن تحريك تغيير في قيمة تم تمريرها إلى مُعدِّل أو نموذج. هذه أدوات قوية جدًا. ولكن هناك أداة أخرى تستخدم أيضًا الرسوم المتحركة - وهي الرسوم المتحركة لمظهر واختفاء العرض.

في المقالة الأخيرة ، تحدثنا عن حقيقة أنه في النمط التعريفي لـ if-else ، فإن هذا لا يتحكم على الإطلاق في تدفق التعليمات البرمجية في وقت التشغيل ، بل هو عرض لشرودنغر. هذه الحاوية تحتوي على طريقتي عرض في نفس الوقت ، والتي تقرر أيهما سيتم عرضها وفقًا لظروف معينة. إذا فوتت كتلة أخرى ، فسيتم عرض EmptyView بدلاً من طريقة العرض الثانية.

يمكن أيضًا التبديل بين العرضين. للقيام بذلك ، استخدم معدل .transition ().

struct TransitionView: View {
    let views: [AnyView] = [AnyView(CustomCircleTestView()), AnyView(SimpleBorderMove())]
    @State var currentViewInd = 0
    var body: some View {
        VStack{
            Spacer()
            ZStack{
                ForEach(views.indices, id: \.self){(ind: Int) in
                    Group{
                        if ind == self.currentViewInd{
                            self.views[ind]
                        }
                    }
                }
            }
            HStack{
                ForEach(views.indices, id: \.self){(ind: Int) in
                    RoundedRectangle(cornerRadius: 10)
                        .fill(ind == self.currentViewInd ? Color.green : Color.gray)
                        .overlay(
                            Text("\(ind + Int(1))"))
                        .onTapGesture{
                            withAnimation{
                                self.currentViewInd = ind
                            }
                    }
                }
            }
                .frame(height: 50)
            Spacer()
        }
    }
}

دعونا نشاهد كيف يعمل. بادئ ذي بدء ، حتى في مرحلة التهيئة للعرض الرئيسي ، قمنا بإنشاء العديد من المشاهدات ووضعها في المصفوفة. الصفيف من نوع AnyView ، لأن عناصر الصفيف يجب أن يكون لها نفس النوع ، وإلا لا يمكن استخدامها في ForEach. نوع نتيجة غير شفاف من المقال السابق ، أتتذكر؟

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

نلف كل عرض من المجموعة في حالة ، ونظهرها فقط إذا كانت نشطة. ومع ذلك ، فإن بناء if-else لا يمكن أن يكون موجودًا هنا ، يأخذه المترجم للتحكم في التدفق ، لذلك نرفق كل هذا في المجموعة بحيث يفهم المترجم بالضبط ما هو عرض ، أو بشكل أكثر دقة ، تعليمات لـ ViewBuilder لإنشاء حاوية اختيارية ConditionalContent <View1، View2>.

الآن ، عند تغيير قيمة currentViewInd الحالية ، يخفي SwiftUI العرض النشط السابق ويظهر العرض الحالي. كيف تحب هذا التنقل داخل التطبيق؟


كل ما عليك فعله هو وضع التغيير الحالي في العرض مع غلاف الرسوم المتحركة ، وسيصبح التبديل بين النوافذ سلسًا.

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

                ForEach(views.indices, id: \.self){(ind: Int) in
                    Group{
                        if ind == self.currentViewInd{
                            self.views[ind]
                                .transition(.scale)
                        }
                    }
                }


لاحظ أن العرض يظهر ويختفي مع نفس الحركة ، فقط اختفاء التمرير في الترتيب العكسي. في الواقع ، يمكننا تخصيص الرسوم المتحركة بشكل فردي لكل من مظهر واختفاء طريقة عرض. يتم استخدام انتقال غير متماثل لهذا الغرض.

                    Group{
                        if ind == self.currentViewInd{
                            self.views[ind]
                               .transition(.asymmetric(
                                    insertion: insertion: AnyTransition.scale(scale: 0.1, anchor: .leading).combined(with: .opacity),
                                    removal: .move(edge: .trailing)))
                        }
                    }


يتم استخدام الرسوم المتحركة ذات الحجم نفسه للظهور على الشاشة ، ولكننا الآن حددنا المعلمات لاستخدامها. لا يبدأ بحجم صفر (نقطة) ، ولكن بحجم 0.1 من الحجم المعتاد. وموقع البدء للنافذة الصغيرة ليس في وسط الشاشة ، ولكنه تحول إلى الحافة اليسرى. بالإضافة إلى ذلك ، لا يوجد انتقال واحد مسؤول عن المظهر ، ولكن اثنين. يمكن دمجها مع .combined (مع :). في هذه الحالة ، أضفنا الشفافية.

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

وكالعادة ، لا أستطيع الانتظار لكتابة نسختي الخاصة من الرسوم المتحركة العابرة. هذا أبسط من النماذج أو المعدلات المتحركة.

struct SpinTransitionModifier: ViewModifier {
    let angle: Double
    let anchor: UnitPoint
    func body(content: Content) -> some View {
        content
            .rotationEffect(Angle(degrees: angle), anchor: anchor)
            .clipped()
    }
}

extension AnyTransition {
    static func spinIn(anchor: UnitPoint) -> AnyTransition {
        .modifier(
            active: SpinTransitionModifier(angle: -90, anchor: anchor),
            identity: SpinTransitionModifier(angle: 0, anchor: anchor))
    }
    static func spinOut(anchor: UnitPoint) -> AnyTransition {
        .modifier(
            active: SpinTransitionModifier(angle: 90, anchor: anchor),
            identity: SpinTransitionModifier(angle: 0, anchor: anchor))
    }
}


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

يحتوي نوع AnyTransition على طريقة تعديل ثابتة ، حيث نمرر معدلين ، ونحصل على كائن AnyTransition يصف الانتقال السلس من حالة إلى أخرى. الهوية هي معدِّل الحالة العادية للعرض المتحرك. نشط هو حالة بداية الرسم المتحرك لظهور العرض ، أو نهاية الرسوم المتحركة للاختفاء ، أي الطرف الآخر من الجزء ، الدول التي سيتم استيفاءها.

لذا ، يعني spinIn أنني سأستخدمه لإظهار العرض من خارج الشاشة (أو المساحة المخصصة للعرض) من خلال التدوير في اتجاه عقارب الساعة حول النقطة المحددة. يعني spinOut أن العرض سيختفي بنفس الطريقة ، يدور حول نفس النقطة ، وأيضًا في اتجاه عقارب الساعة.

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

تعتمد جميع الرسوم المتحركة على ميكانيكا التعديل القياسية. إذا قمت بكتابة مُعدِّل مخصص بالكامل ، يجب عليك تنفيذ متطلبات بروتوكول AnimatableModifier ، كما فعلنا مع TwoParameterBorder ، أو استخدام المعدلات المضمنة بداخله التي توفر الرسوم المتحركة الافتراضية الخاصة بهم. في هذه الحالة ، اعتمدت على الرسوم المتحركة المضمنة .rotationEffect () داخل مُعدِّل SpinTransitionModifier.

يوضح معدّل .transition () فقط ما يجب اعتباره كنقطة بداية الرسوم المتحركة ونهايتها. إذا احتجنا إلى طلب حالة AnimatableData قبل بدء الرسم المتحرك ، لطلب معدِّل AnimatableData للحالة الحالية ، وحساب الفرق ، ثم تحريك الانخفاض من 1 إلى 0 ، ثم .transition () يغير البيانات الأصلية فقط. أنت لست مرتبطًا بحالة العرض الخاصة بك ؛ أنت لا تعتمد عليها. أنت تحدد بوضوح الحالة الأولية والنهائية بنفسك ، حيث تحصل منها على AnimatableData ، وتحسب الفرق وتحركه. ثم ، في نهاية الرسوم المتحركة ، تظهر طريقة العرض الحالية في المقدمة.

بالمناسبة ، الهوية عبارة عن أداة تعديل ستظل مطبقة على طريقة العرض الخاصة بك في نهاية الرسوم المتحركة. خلاف ذلك ، سيؤدي خطأ هنا إلى قفزات في نهاية الرسوم المتحركة المظهر ، وبداية الرسوم المتحركة للاختفاء. لذلك يمكن اعتبار الانتقال كـ "اثنين في واحد" - تطبيق مُعدِّل معين مباشرةً على View + القدرة على تحريك تغييراته عندما يظهر العرض ويختفي.

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

لرؤية كيفية حدوث التغيير نفسه بشكل أفضل ، قمت باستبدال طريقة العرض الاختبارية بمربعات أولية ، وموقعة بأرقام ، ومؤطرة.

                    Group{
                        if ind == self.currentViewInd{
                            //self.views[ind]
                            Rectangle()
                                .fill(Color.gray)
                                .frame(width: 100, height: 100)
                                .border(Color.black, width: 2)
                                .overlay(Text("\(ind + 1)"))
                              .transition(.asymmetric(
                                  insertion: .spinIn(anchor: .bottomTrailing),
                                  removal: .spinOut(anchor: .bottomTrailing)))
                        }
                    }


ولجعل هذه الحركة أفضل ، قمت بإزالة .clipped () من مُعدِّل SpinTransitionModifier:

struct SpinTransitionModifier: ViewModifier {
    let angle: Double
    let anchor: UnitPoint
    func body(content: Content) -> some View {
        content
            .rotationEffect(Angle(degrees: angle), anchor: anchor)
            //.clipped()
    }
}


بالمناسبة ، نحتاج الآن إلى SpinTransitionModifier في أداة التعديل الخاصة بنا تمامًا. تم إنشاؤه فقط من أجل الجمع بين المعدلين ، rotationEffect وقص () في واحد ، بحيث لا تتجاوز الرسوم المتحركة للتدوير النطاق المحدد للعرض الخاص بنا. الآن ، يمكننا استخدام .rotationEffect () مباشرة داخل .modifier () ، لا نحتاج إلى وسيط في شكل SpinTransitionModifier.

عندما يموت العرض


نقطة مثيرة للاهتمام هي دورة حياة العرض إذا تم وضعها في if-else. لا يتم تخزين العرض في الذاكرة ، على الرغم من أنه تم بدء تشغيله وتسجيله كعنصر صفيف. كل لهاحالةيتم إعادة تعيين المعلمات إلى الإعدادات الافتراضية في المرة التالية التي تظهر فيها على الشاشة. هذا هو تقريبا نفس التهيئة. على الرغم من حقيقة أن بنية الكائن نفسها لا تزال موجودة ، إلا أن التجسيد أزالها من مجال رؤيتها ، لأنها ليست موجودة. من ناحية ، يقلل هذا من استخدام الذاكرة. إذا كان لديك عدد كبير من طرق العرض المعقدة في المصفوفة ، فسيتعين على العرض أن يرسمها جميعًا باستمرار ، ويتفاعل مع التغييرات - وهذا يؤثر سلبًا على الأداء. إذا لم أكن مخطئًا ، فهذا هو الحال قبل تحديث Xcode 11.3. الآن ، يتم إلغاء تحميل طرق العرض غير النشطة من ذاكرة العرض.

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

وبالعودة إلى دورة الحياة ، تجدر الإشارة أيضًا إلى أن معدِّل .onAppear {} ، إذا تم تسجيله داخل طريقة العرض هذه ، يعمل فورًا بعد تغيير الحالة وظهور العرض على الشاشة ، حتى قبل بدء الرسم المتحرك. وفقًا لذلك ، يتم تشغيل onDisappear {} بعد نهاية الرسوم المتحركة للاختفاء. ضع ذلك في الاعتبار إذا كنت تخطط لاستخدامها مع الرسوم المتحركة الانتقالية.

ماذا بعد؟


Phew اتضح أنه ضخم للغاية ، ولكن بالتفصيل ، وآمل ، بوضوح. بصراحة ، كنت آمل أن أتحدث عن الرسوم المتحركة بألوان قوس قزح كجزء من مقال واحد ، لكنني لم أستطع التوقف في الوقت المناسب مع التفاصيل. لذا انتظر المتابعة.

الجزء التالي ينتظرنا:

  • استخدام التدرجات الخطية ، الدائرية والزاوية - كل شيء سيكون مفيدًا
  • اللون ليس لونًا على الإطلاق: اختر بحكمة.
  • رسوم متحركة متكررة: كيفية البدء وكيفية التوقف ، وكيفية التوقف فورًا (بدون رسوم متحركة ، تغيير الرسوم المتحركة - نعم ، هناك واحد أيضًا)
  • الرسوم المتحركة للبث الحالي: الأولويات ، والإلغاء ، والرسوم المتحركة المختلفة لكائنات مختلفة
  • تفاصيل عن توقيت الرسوم المتحركة: سنقود التوقيتات في الذيل واللباس ، وصولًا إلى تنفيذنا الخاص للتوقيت Curve (أوه ، احتفظ بي سبعة :))
  • كيفية معرفة اللحظة الحالية للرسوم المتحركة التي تم تشغيلها
  • إذا لم يكن SwiftUI كافيًا

سأتحدث عن كل هذا بالتفصيل باستخدام مثال إنشاء الرسوم المتحركة قوس قزح ، كما في الصورة:


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

All Articles