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

SwiftUI में एनिमेटेड ऑनबोर्डिंग मैस्कॉट

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

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

SwiftUI में ऑनबोर्डिंग मैस्कॉट को एनिमेट करने के लिए, pages के बाहर रहने वाले एक ही मैस्कॉट view के नीचे .tabViewStyle(.page) वाला एक TabView रखें। जब चुना गया page बदले, तो मैस्कॉट की mood property सेट करें, और खास पलों पर वन-शॉट ट्रिगर फ़ायर करें। एक ही view रखने से एनिमेशन लगातार चलता रहता है, हर swipe पर दोबारा लोड नहीं होता।

मुख्य बातें

  • एक ही मैस्कॉट view को TabView के बाहर रखें; हर page के अंदर रखा मैस्कॉट हर swipe पर दोबारा लोड होकर शुरू से चलता है।
  • हर page को एक लूपिंग मूड से जोड़ें और ट्रिगर सिर्फ़ खास पलों के लिए इस्तेमाल करें (स्वागत, अनुमति मिलना, पूरा होना)।
  • PageTabViewStyle iOS 14 से उपलब्ध है, जो Rive के रनटाइम का भी न्यूनतम वर्ज़न है।
  • Reduce Motion चालू होने पर हर page पर एक स्थिर पोज़ दिखाएँ और वन-शॉट रिएक्शन छोड़ दें।

TabView(.page) से paged ऑनबोर्डिंग कैसे बनाएँ?

selection से bind किया गया और .page स्टाइल वाला TabView आपको page indicator के साथ swipe होने वाले pages देता है, जो iOS 14 से उपलब्ध है। Pages को एक enum के रूप में मॉडल करें ताकि हर page का अपना टेक्स्ट और मैस्कॉट स्टेट हो:

enum OnboardingPage: Int, CaseIterable, Identifiable {
    case welcome, feature, permission, paywall
    var id: Int { rawValue }

    var title: String {
        switch self {
        case .welcome: return "Meet Kiko"
        case .feature: return "Snap a receipt, Kiko sorts it"
        case .permission: return "Allow notifications?"
        case .paywall: return "Go Pro"
        }
    }
}

struct OnboardingPages: View {
    @Binding var page: OnboardingPage

    var body: some View {
        TabView(selection: $page) {
            ForEach(OnboardingPage.allCases) { item in
                Text(item.title)
                    .font(.title2.bold())
                    .multilineTextAlignment(.center)
                    .padding(.horizontal, 32)
                    .tag(item)
            }
        }
        .tabViewStyle(.page)
        .indexViewStyle(.page(backgroundDisplayMode: .always))
    }
}

.tag(item) का टाइप selection binding के टाइप जैसा ही होना चाहिए, वरना swipe करने पर page अपडेट नहीं होगा।

सभी pages में एक ही मैस्कॉट view को कैसे चालू रखें?

मैस्कॉट को VStack में TabView के ऊपर रखें, ForEach के अंदर नहीं। Swipe करते समय pages बनते और हटते रहते हैं; उनके अंदर रखा मैस्कॉट हर बार अपनी फ़ाइल दोबारा लोड करेगा और entry स्टेट से फिर शुरू होगा। बाहर रखने पर वह चलता रहता है और बस स्टेट बदलता है।

नीचे दिया गया controller Rive का नया Apple रनटाइम इस्तेमाल करता है और bound view model instance को संभालकर रखता है, जो बाद में values बदलने के लिए ज़रूरी है:

import SwiftUI
import RiveRuntime

@MainActor
final class MascotController: ObservableObject {
    @Published private(set) var rive: Rive?
    private var instance: ViewModelInstance?

    func load(initialMood: Int) async {
        guard rive == nil else { return }
        do {
            let worker = try await Worker()
            let file = try await File(source: .local("mascot", Bundle.main), worker: worker)
            let artboard = try await file.createArtboard()
            let instance = try await file.createViewModelInstance(
                .viewModelDefault(from: .artboardDefault(artboard))
            )
            instance.setValue(of: NumberProperty(path: "mood"), to: Float(initialMood))
            let machine = try await artboard.createStateMachine(
                "CharacterViewModelMachine",
                binding: instance
            )
            self.instance = instance
            rive = try await Rive(file: file, artboard: artboard, stateMachine: machine)
        } catch {
            print("Mascot failed to load: \(error)")
        }
    }

    func setMood(_ mood: Int) {
        instance?.setValue(of: NumberProperty(path: "mood"), to: Float(mood))
    }

    func fire(_ trigger: String) {
        instance?.fire(trigger: TriggerProperty(path: trigger))
    }
}

View दिखने से पहले पहला मूड सेट करने से फ़ाइल के default स्टेट का एक फ़्रेम नहीं दिखता, यह बात Rive की migration गाइड में साफ़ तौर पर कही गई है।

हर ऑनबोर्डिंग page को मैस्कॉट के मूड से कैसे जोड़ें?

पहले mapping को एक टेबल के रूप में तय करें, फिर उसे enum में लिखें। लूप बताते हैं कि यूज़र page पर क्या कर रहा है; ट्रिगर खास पलों को दर्शाते हैं।

Pageलूप (mood)ट्रिगर
स्वागतwave (2)पहली बार दिखने पर enter
फ़ीचरthinking (3)कोई नहीं
अनुमतिlistening (6)system prompt के बाद yes या no
Paywallhappy (1)कोई नहीं (शांत रखें)
पूराidle (0)success
extension OnboardingPage {
    var mood: Int {
        switch self {
        case .welcome: return 2
        case .feature: return 3
        case .permission: return 6
        case .paywall: return 1
        }
    }
}

struct OnboardingView: View {
    @StateObject private var mascot = MascotController()
    @State private var page: OnboardingPage = .welcome

    var body: some View {
        VStack(spacing: 0) {
            RiveUIViewRepresentable(rive: mascot.rive)
                .frame(height: 260)
                .accessibilityHidden(true)
            OnboardingPages(page: $page)
        }
        .task { await mascot.load(initialMood: page.mood) }
        .onChange(of: page) { _, newPage in
            mascot.setMood(newPage.mood)
        }
    }
}

दो पैरामीटर वाला onChange(of:) closure iOS 17 का रूप है; iOS 14 से 16 पर एक पैरामीटर वाला वर्ज़न इस्तेमाल करें। चूँकि transitions स्टेट मशीन संभालती है, इसलिए तेज़ी से आगे-पीछे swipe करना सुरक्षित है: आखिरी value ही लागू होती है।

तीन ऑनबोर्डिंग स्क्रीन जिनमें एक मैस्कॉट हाथ हिलाता है, यूज़र के टाइप करते समय सुनता है, फिर जश्न मनाता है
एक मैस्कॉट के साथ ऑनबोर्डिंग: हर स्टेप एक मूड सेट करता है या ट्रिगर फ़ायर करता है।

Page दिखने पर वन-शॉट रिएक्शन कैसे फ़ायर करें?

ट्रिगर events से फ़ायर करें, layout से नहीं। अच्छे मौके हैं: loading पूरा होना (enter), permission prompt का नतीजा, और आखिरी बटन:

Button("Allow notifications") {
    Task {
        let center = UNUserNotificationCenter.current()
        let granted = (try? await center.requestAuthorization(options: [.alert, .badge])) ?? false
        mascot.fire(granted ? "yes" : "no")
        withAnimation { page = .paywall }
    }
}

(इसके लिए import UserNotifications ज़रूरी है।) किसी page के onAppear में ट्रिगर फ़ायर करने से बचें: TabView आसपास के pages पहले ही बना सकता है, इसलिए onAppear यूज़र के page देखने से पहले ही चल सकता है। ऊपर दिए onChange की तरह page selection पर प्रतिक्रिया देना भरोसेमंद है।

वन-शॉट रिएक्शन छोटे और कम रखें। जो मैस्कॉट हर swipe पर जश्न मनाता है, तीसरे page तक उसका कोई मतलब नहीं रह जाता; success को असली अंत के लिए बचाकर रखें।

छह वन-शॉट रिएक्शन: success, error, yes, no, enter और exit
वन-शॉट रिएक्शन ट्रिगर से फ़ायर होते हैं और खत्म होने पर मौजूदा मूड पर लौट आते हैं।

ऑनबोर्डिंग में Reduce Motion का ध्यान कैसे रखें?

@Environment(\.accessibilityReduceMotion) पढ़ें। जब यह true हो, तो live view की जगह हर page पर एक स्थिर इमेज दिखाएँ और ट्रिगर छोड़ दें। स्थिर इमेज के बीच छोटा crossfade, मूवमेंट की जगह मोशन कम करने का एक आम विकल्प है।

struct OnboardingMascot: View {
    @Environment(\.accessibilityReduceMotion) private var reduceMotion
    @ObservedObject var mascot: MascotController
    let page: OnboardingPage

    var body: some View {
        if reduceMotion {
            Image("mascot-mood-\(page.mood)")
                .resizable()
                .scaledToFit()
                .id(page)
                .transition(.opacity)
        } else {
            RiveUIViewRepresentable(rive: mascot.rive)
        }
    }
}

स्थिर इमेज उसी कैरेक्टर के वीडियो से एक्सपोर्ट किए गए फ़्रेम हो सकते हैं, ताकि कम मोशन वाला वर्ज़न एनिमेटेड वर्ज़न से मेल खाए। मैस्कॉट जो भी जानकारी देता है, जैसे "अनुमति मिल गई", वह टेक्स्ट में भी दिखनी चाहिए। और पैटर्न SwiftUI और UIKit में Reduce Motion में हैं।

क्या ऑनबोर्डिंग splash screen से शुरू हो या सीधे कंटेंट पर जाए?

Apple की Human Interface Guidelines साफ़ कहती हैं: लॉन्च स्क्रीन "आपके ऐप की पहली स्क्रीन से लगभग एक जैसी" होनी चाहिए और यह "branding का मौका नहीं है"। अगर आप branded पल चाहते हैं, तो HIG इसे "आपके ऑनबोर्डिंग flow की शुरुआत में" दिखाने का सुझाव देती है।

इसलिए मैस्कॉट की एंट्री के लिए स्वागत page सबसे स्वाभाविक जगह है: OS की लॉन्च स्क्रीन सादा बैकग्राउंड दिखाती है, पहला ऑनबोर्डिंग page उसी बैकग्राउंड के साथ आता है, और यूज़र के पढ़ते समय मैस्कॉट enter चलाकर अपने wave लूप में आ जाता है। अलग splash screen की ज़रूरत नहीं है। अलग intro का code पैटर्न SwiftUI में एनिमेटेड splash screen में है।

अगर आपके पास अभी कोई एनिमेटेड कैरेक्टर नहीं है, तो Charmotion अपलोड किए गए मैस्कॉट या लोगो से एक बनाता है: 17 एनिमेशन, आमतौर पर कुछ ही मिनटों में। ऊपर इस्तेमाल किए गए mood और ट्रिगर ढाँचे वाली .riv, साथ में दोनों Rive रनटाइम के लिए SwiftUI फ़ाइलें, हर पेड प्लान के साथ मिलती हैं ($20/माह से); फ़्री प्लान में कोई कैरेक्टर सेट नहीं है। कीमतें देखें।

SwiftUI ऑनबोर्डिंग में एनिमेटेड मैस्कॉट जोड़ें

  1. Pages मॉडल करें. Pages का एक enum बनाएँ, जिसमें हर page का title और मूड नंबर हो।
  2. Pager बनाएँ. .tabViewStyle(.page) के साथ TabView(selection:) इस्तेमाल करें और हर page को उसके enum case से tag करें।
  3. एक मैस्कॉट जोड़ें. RiveUIViewRepresentable(rive:) को VStack में TabView के ऊपर रखें, एक ऐसे controller के साथ जो view model instance को संभालकर रखे।
  4. मूड sync करें. .onChange(of: page) में NumberProperty(path: "mood") को page के मूड पर सेट करें।
  5. पलों पर ट्रिगर करें. enter, yes/no और success ट्रिगर असली events से फ़ायर करें, जैसे permission का नतीजा।
  6. Reduce Motion संभालें. जब accessibilityReduceMotion true हो, तो live view की जगह हर page पर एक स्थिर इमेज दिखाएँ।

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

मेरा मैस्कॉट हर ऑनबोर्डिंग page पर दोबारा क्यों शुरू होता है?

वह TabView के pages के अंदर है, इसलिए SwiftUI हर page के लिए नया view, और नई Rive file, बनाता है। मैस्कॉट को TabView के बाहर ले जाएँ और उसका controller parent view पर @StateObject में रखें। फिर page बदलने पर सिर्फ़ mood value बदलती है।

क्या ऑनबोर्डिंग मैस्कॉट के लिए Rive की जगह Lottie इस्तेमाल कर सकते हैं?

हाँ, अगर हर page बस अपनी क्लिप चलाता है। LottieView में data-bound स्टेट मशीन नहीं है, इसलिए फ़ाइलें या marker segments के बीच आपको खुद switch करना होगा, और जब तक आप उन्हें डिज़ाइन न करें, उनके बीच transitions सीधे cut होंगे। Rive एक ही फ़ाइल के अंदर मूड के बीच blending संभालता है।

क्या मैस्कॉट VoiceOver को दिखना चाहिए?

आमतौर पर नहीं। ऑनबोर्डिंग में संदेश टेक्स्ट देता है, इसलिए मैस्कॉट पर accessibilityHidden(true) लगाएँ। अगर कैरेक्टर कुछ ऐसा बताता है जो टेक्स्ट में नहीं है, तो उसे एक accessibility label दें जो एनिमेशन के बजाय स्टेट बताए, जैसे "Setup पूरा हुआ"।

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

स्रोत

संबंधित गाइड