कंटेंट पर जाएँ
charmotion

SwiftUI में PhaseAnimator बनाम KeyframeAnimator

Charmotion टीम द्वारा · अपडेट:

iOS & Swift गाइड का कवर इलस्ट्रेशन: SwiftUI में PhaseAnimator बनाम KeyframeAnimator, Charmotion के डेमो कैरेक्टर के साथ

जब एनिमेशन कुछ अलग-अलग स्टेट से होकर गुज़रे, जैसे rest, lift और land, और हर एक का अपना curve हो, तो PhaseAnimator इस्तेमाल करें। जब scale, rotation और offset को अलग-अलग timed tracks चाहिए, तो KeyframeAnimator इस्तेमाल करें। दोनों iOS 17, macOS 14, tvOS 17, watchOS 10 और visionOS 1 में आए, और दोनों लूप में या ट्रिगर पर चल सकते हैं।

मुख्य बातें

  • PhaseAnimator phases के एक क्रम से गुज़रता है; हर phase के लिए आप जो एनिमेशन लौटाते हैं, उससे SwiftUI उनके बीच interpolate करता है।
  • KeyframeAnimator एक value type को स्पष्ट durations वाले समानांतर KeyframeTracks पर चलाता है।
  • ट्रिगर के बिना phase animator लगातार चलता रहता है; keyframeAnimator(initialValue:repeating:…) default रूप से दोहराता है।
  • हर value बदलने पर एक बार चलाने के लिए दोनों में से किसी को भी trigger: दें।
  • ऐसे कैरेक्टर के लिए जिसके कई स्टेट हों और जिस पर डिज़ाइनर बार-बार काम करते हों, hand-coded tracks की तुलना में Rive स्टेट मशीन बेहतर है।

PhaseAnimator क्या करता है?

PhaseAnimator (और .phaseAnimator modifier) phases का एक क्रम लेता है, मौजूदा phase के लिए आपका content render करता है, और अगले phase तक एनिमेट करता है। Apple का signature:

func phaseAnimator<Phase>(
    _ phases: some Sequence,
    content: @escaping (PlaceholderContentView<Self>, Phase) -> some View,
    animation: @escaping (Phase) -> Animation? = { _ in .default }
) -> some View

एक badge जो ऊपर उठता है और नीचे आता है:

enum BadgePhase: CaseIterable {
    case rest, lift, land
}

struct NewBadge: View {
    var body: some View {
        Image(systemName: "star.fill")
            .font(.largeTitle)
            .foregroundStyle(.yellow)
            .phaseAnimator(BadgePhase.allCases) { content, phase in
                content
                    .scaleEffect(phase == .lift ? 1.2 : 1)
                    .offset(y: phase == .lift ? -12 : 0)
                    .rotationEffect(.degrees(phase == .land ? -6 : 0))
            } animation: { phase in
                switch phase {
                case .rest: return .smooth(duration: 0.6)
                case .lift: return .spring(duration: 0.3, bounce: 0.4)
                case .land: return .bouncy
                }
            }
    }
}

एनिमेशन closure को वह phase मिलता है जिसकी ओर एनिमेशन हो रहा है। Phases को poses की तरह समझें: आप हर pose बताते हैं, और बीच के फ़्रेम SwiftUI संभालता है।

KeyframeAnimator क्या करता है?

KeyframeAnimator एक ही value को, आमतौर पर कई properties वाले struct को, आपके तय किए गए स्पष्ट timing वाले tracks पर एनिमेट करता है। हर KeyframeTrack एक key path को target करता है और बाकी tracks के साथ समानांतर चलता है।

func keyframeAnimator<Value>(
    initialValue: Value,
    repeating: Bool = true,
    content: @escaping (PlaceholderContentView<Self>, Value) -> some View,
    keyframes: @escaping (Value) -> some Keyframes
) -> some View

एक स्थिर मैस्कॉट इमेज पर wobble, जो एक counter से ट्रिगर होता है:

struct Wobble {
    var scale: CGFloat = 1
    var rotation: Angle = .zero
    var offsetY: CGFloat = 0
}

struct WobblingMascot: View {
    @State private var taps = 0

    var body: some View {
        Image("mascot-still")
            .resizable()
            .scaledToFit()
            .frame(width: 160)
            .keyframeAnimator(initialValue: Wobble(), trigger: taps) { content, value in
                content
                    .scaleEffect(value.scale)
                    .rotationEffect(value.rotation)
                    .offset(y: value.offsetY)
            } keyframes: { _ in
                KeyframeTrack(\.scale) {
                    SpringKeyframe(1.15, duration: 0.2)
                    SpringKeyframe(1.0, duration: 0.3)
                }
                KeyframeTrack(\.rotation) {
                    CubicKeyframe(.degrees(-8), duration: 0.15)
                    CubicKeyframe(.degrees(8), duration: 0.15)
                    CubicKeyframe(.zero, duration: 0.2)
                }
                KeyframeTrack(\.offsetY) {
                    LinearKeyframe(-10, duration: 0.2)
                    SpringKeyframe(0, duration: 0.3)
                }
            }
            .onTapGesture { taps += 1 }
    }
}

Keyframe के प्रकार interpolation तय करते हैं: LinearKeyframe, CubicKeyframe, SpringKeyframe और MoveKeyframe (बिना interpolation वाली सीधी छलांग)।

phases और keyframes में से कब क्या इस्तेमाल करें?

PhaseAnimatorKeyframeAnimatorRive स्टेट मशीन
सोचने का तरीकाposes की एक सूचीहर property के लिए समय-आधारित tracksडिज़ाइनर का बनाया रिग, स्टेट और transitions के साथ
टाइमिंग कंट्रोलहर phase के लिए एक animation curveहर keyframe और हर track के लिए सटीक अवधिeditor में बनाया जाता है
क्या properties साथ-साथ बदलती हैं?हाँ, हर phase पर सभी बदलती हैंनहीं, tracks अलग-अलग चलते हैंहर bone और layer के लिए
इसे कौन एडिट करता हैडेवलपरडेवलपरडिज़ाइनर, code से नियंत्रित
किसके लिए बेस्टPulse, ध्यान खींचने वाले हल्के nudge, सरल लूपWobble, bounce, कोरियोग्राफ़ किए गए वन-शॉटकई मूड और रिएक्शन वाले कैरेक्टर

एक आसान टेस्ट: अगर आप मोशन को "A पर जाएँ, फिर B पर, फिर वापस" की तरह बता सकते हैं, तो phases इस्तेमाल करें। अगर आप इसे "0.2 s पर scale overshoot करता है और rotation दो बार झूलता है" की तरह बताते हैं, तो keyframes इस्तेमाल करें।

निर्णय तालिका: UI फ़ीडबैक के लिए built-in API, आइकन के लिए आइकन इफ़ेक्ट, कैरेक्टर के लिए Rive स्टेट मशीन, मार्केटिंग क्लिप के लिए वीडियो
काम के हिसाब से एनिमेशन का तरीका चुनें।

हर animator को लूप या ट्रिगर कैसे करें?

दोनों modifiers के दो मोड हैं:

  • लगातार। बिना ट्रिगर के .phaseAnimator(phases) { … } phases में हमेशा घूमता रहता है। .keyframeAnimator(initialValue:repeating:content:keyframes:) तब तक दोहराता है जब तक repeating true है, जो इसका default है।
  • ट्रिगर से। .phaseAnimator(phases, trigger: value) { … } हर बार value बदलने पर phases को एक बार चलाता है, फिर पहले phase पर रुक जाता है। .keyframeAnimator(initialValue:trigger:content:keyframes:) हर बदलाव पर keyframes को एक बार चलाता है।
// Once per successful save
.phaseAnimator(BadgePhase.allCases, trigger: saveCount) { content, phase in
    content.scaleEffect(phase == .lift ? 1.2 : 1)
}

// Repeating idle breathing, paused under Reduce Motion
.keyframeAnimator(initialValue: Wobble(), repeating: !reduceMotion) { content, value in
    content.scaleEffect(value.scale)
} keyframes: { _ in
    KeyframeTrack(\.scale) {
        CubicKeyframe(1.03, duration: 1.2)
        CubicKeyframe(1.0, duration: 1.2)
    }
}

trigger value Equatable होनी चाहिए; बढ़ता हुआ Int सबसे आसान विकल्प है। जब यूज़र Reduce Motion चालू करे, तो लगातार चलने वाले लूप रुक जाने चाहिए; इसके लिए @Environment(\.accessibilityReduceMotion) पढ़ें।

हर phase या keyframe के अंदर टाइमिंग कैसे कंट्रोल करें?

दोनों animators टाइमिंग को अलग तरह से देते हैं, और अक्सर यही फ़ैसला करने वाली बात होती है।

PhaseAnimator: हर phase की अवधि उतनी होती है जितना लौटाया गया Animation लेता है। duration: 0.3 वाला spring लगभग 0.3 s में phase तक पहुँचता है; .linear(duration: 1) एक सेकंड लेता है। किसी pose को रोके रखने के लिए अगले phase के एनिमेशन में delay जोड़ें:

} animation: { phase in
    switch phase {
    case .rest: return .easeOut(duration: 0.4).delay(1.5)   // pause before lifting again
    case .lift: return .spring(duration: 0.3, bounce: 0.4)
    case .land: return .bouncy
    }
}

किसी phase के लिए nil लौटाने पर एनिमेशन बिना चले सीधे उस phase पर पहुँच जाता है।

KeyframeAnimator: हर keyframe की एक स्पष्ट duration होती है, और किसी track की कुल लंबाई उसके keyframes का जोड़ होती है। Tracks साथ-साथ चलते हैं, इसलिए एनिमेशन तब खत्म होता है जब सबसे लंबा track खत्म होता है। अगर कोई track छोटा है, तो उसकी property बाकी tracks खत्म होने तक अपनी आखिरी value पर बनी रहती है। सभी tracks में किसी पल को एक साथ लाने के लिए (जैसे 0.2 s पर squash का सबसे निचला बिंदु), हर track में keyframe की अवधियों का जोड़ उसी समय के बराबर रखें।

क्योंकि keyframes data हैं, आप KeyframeTimeline से उन्हें view के बाहर भी sample कर सकते हैं, जो tests के लिए या उसी curve से non-view values चलाने के लिए उपयोगी है।

कौन से OS versions PhaseAnimator और KeyframeAnimator को सपोर्ट करते हैं?

Apple का documentation दोनों types और उनके modifiers के लिए एक जैसी availability बताता है:

प्लेटफ़ॉर्मन्यूनतम
iOS / iPadOS17.0
macOS14.0
tvOS17.0
watchOS10.0
visionOS1.0

अगर आप अभी भी iOS 16 सपोर्ट करते हैं, तो call को if #available(iOS 17, *) में रखें और fallback के तौर पर withAnimation chains या static view इस्तेमाल करें। watchOS वाली entry पर ध्यान दें: ये animators Apple Watch पर कैरेक्टर एनिमेट करने के गिने-चुने तरीकों में से हैं, जहाँ Rive और Lottie का कोई official सपोर्ट नहीं है (देखें watchOS और visionOS पर कैरेक्टर एनिमेशन)।

कैरेक्टर के लिए native SwiftUI एनिमेशन कब काफ़ी नहीं होता?

दोनों animators पूरे view को एक साथ बदलते हैं: scale, rotate, offset, opacity, color। ये हाथ नहीं मोड़ते, आँख नहीं झपकाते और lip-sync नहीं करते, क्योंकि एक flat image में हिलाने लायक अलग हिस्से नहीं होते। आप कैरेक्टर को layered images में बाँटकर हर हिस्से को एनिमेट कर सकते हैं, लेकिन फिर हर नई स्टेट का मतलब है ज़्यादा code और हाथ से ज़्यादा टाइमिंग tune करना।

डिज़ाइनर के बनाए format पर तब जाएँ जब:

  • कैरेक्टर को दो-तीन से ज़्यादा स्टेट चाहिए (idle, thinking, success, error, talking…)।
  • स्टेट को एनिमेशन के बीच में एक-दूसरे में घुलना हो, अचानक cut नहीं।
  • motion पर डेवलपर नहीं, डिज़ाइनर को काम करना हो।
  • वही कैरेक्टर Android या वेब पर भी चलना हो।

Rive ये सब एक स्टेट मशीन से संभालता है जिसे आपका code कुछ values के ज़रिए चलाता है (देखें SwiftUI में Rive data binding)। Native animators इसके साथ भी अच्छे से चलते हैं: Rive मैस्कॉट के बगल में किसी badge पर phaseAnimator pulse का कोई खर्च नहीं है और इसके लिए किसी asset की ज़रूरत नहीं।

अक्सर पूछे जाने वाले सवाल

क्या PhaseAnimator और KeyframeAnimator watchOS पर चल सकते हैं?

हाँ। Apple दोनों को watchOS 10 और उसके बाद के लिए बताता है, साथ में iOS 17, macOS 14, tvOS 17 और visionOS 1। इसलिए Apple Watch पर किसी सरल कैरेक्टर या badge को एनिमेट करने का यह व्यावहारिक तरीका है, जहाँ Rive और Lottie packages watchOS सपोर्ट declare नहीं करते।

PhaseAnimator को हमेशा लूप होने से कैसे रोकें?

एक trigger value दें: .phaseAnimator(phases, trigger: count)। फिर यह हर बार count बदलने पर phases को एक बार चलाता है और पहले phase पर रुक जाता है। बिना ट्रिगर के, जब तक view स्क्रीन पर है, SwiftUI phases में लगातार घूमता रहता है।

क्या KeyframeAnimator और CSS keyframes एक ही चीज़ हैं?

विचार मिलता-जुलता है, संरचना अलग है। CSS keyframes सभी properties को percentage stops पर सेट करते हैं। SwiftUI का KeyframeAnimator हर property को अपना track देता है, जिसमें अवधि सेकंड में होती है और interpolation आप चुनते हैं, इसलिए एक ही एनिमेशन में scale और rotation अलग-अलग टाइमिंग पर चल सकते हैं। Springs भी keyframes के रूप में उपलब्ध हैं, जिन्हें CSS सीधे व्यक्त नहीं कर सकता।

इस गाइड से झटपट जवाब

स्रोत

संबंधित गाइड