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

SwiftUI में Rive Data Binding (नया Apple रनटाइम)

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

iOS और Swift गाइड का कवर इलस्ट्रेशन: SwiftUI में Rive Data Binding (नया Apple रनटाइम), Charmotion डेमो कैरेक्टर के साथ

SwiftUI में Rive Data Binding इस्तेमाल करने के लिए एक Worker और File बनाएँ, file.createViewModelInstance से view model instance बनाएँ, उसे artboard.createStateMachine(name, binding: instance) से bind करें, और RiveUIViewRepresentable(rive:) के ज़रिए Rive(file:artboard:stateMachine:) render करें। Instance को संभालकर रखें, फिर उस पर setValue(of:to:) और fire(trigger:) कॉल करें।

मुख्य बातें

  • नया Apple रनटाइम rive-ios 6.24.0 में आया और Swift Concurrency इस्तेमाल करता है; setup calls async हैं, और सभी calls main actor पर चलती हैं।
  • इसमें स्टेट मशीन इनपुट नहीं हैं। Numbers, booleans और ट्रिगर एक व्यू मॉडल में रहते हैं जिसे आप explicitly bind करते हैं।
  • ViewModelInstance को खुद संभालकर रखें: स्टेट मशीन में bound instances के लिए कोई getter नहीं है।
  • NumberProperty.Value का टाइप Float है, इसलिए Int मूड को Float(mood) से convert करें।
  • नए code में deprecated Rive(file:…dataBind:) initializer इस्तेमाल न करें।

नए Rive Apple रनटाइम में क्या बदला?

Rive के Apple पैकेज (RiveRuntime, rive-app/rive-ios से) में अब दो API हैं। Legacy रनटाइम RiveViewModel पर आधारित है और Rive के शब्दों में "currently single-threaded on the main thread" है। वर्ज़न 6.24.0 में आया नया रनटाइम "a Swift-first API leveraging Swift Concurrency, with improved multi-threading support" है। यह लिखते समय (अक्टूबर 2026) latest release 6.28.1 है; 6.28.0 में global view models और blob data binding जोड़े गए।

व्यवहार में तीन अंतर मायने रखते हैं:

  • सब कुछ asynchronous है। Worker, file, artboard, स्टेट मशीन और Rive object बनाने में try await इस्तेमाल होता है। Rive अब भी चाहता है कि calls main actor पर हों; भारी काम worker के background thread पर होता है।
  • कोई इनपुट, events या linear animations नहीं। Migration guide में स्टेट मशीन इनपुट और event listeners को सिर्फ़ legacy के लिए बताया गया है, और उन्हें वापस लाने की "no current plans to reintroduce" (फ़िलहाल कोई योजना नहीं) है। Linear animations को स्टेट मशीन में wrap करना होगा।
  • Data Binding ही contract है। आपका code typed properties (NumberProperty, TriggerProperty आदि) के ज़रिए view model instance से बात करता है, जिन्हें path से address किया जाता है।

अगर आपकी .riv फ़ाइल सिर्फ़ इनपुट expose करती है, तो या तो legacy रनटाइम पर रहें (देखें SwiftUI में Rive) या पहले editor में Menu → Convert Inputs to ViewModels इस्तेमाल करें।

Worker, File, Artboard और Rive आपस में कैसे जुड़ते हैं?

नया रनटाइम RiveViewModel के काम को पाँच objects में बाँटता है। हर object पिछले वाले से बनता है:

Objectकिससे बनता हैकाम
Workertry await Worker()Background thread और शेयर किए गए out-of-band assets (images, fonts, audio) का मालिक।
Filetry await File(source:worker:)पार्स की गई .riv फ़ाइल। Source के रूप में .local(name, bundle), .url(URL) या .data(Data) इस्तेमाल होते हैं।
Artboardtry await file.createArtboard()जिस canvas को ड्रॉ करना है; डिफ़ॉल्ट के अलावा किसी दूसरे आर्टबोर्ड के लिए उसका नाम पास करें।
StateMachinetry await artboard.createStateMachine(name, binding: instance)लॉजिक चलाता है और बाइंड किए गए view model instance को पढ़ता है।
Rivetry await Rive(file:artboard:stateMachine:)view को दी जाने वाली view configuration (fit, background)।

Rive के docs के अनुसार Worker "Rive के इस्तेमाल की पूरी अवधि तक alive रहना चाहिए" और File अपने worker का strong reference रखता है। एक worker लगभग एक background thread के बराबर है; कई भारी graphics के लिए आप हर फ़ाइल को अपना अलग worker दे सकते हैं, या एक ही worker शेयर कर सकते हैं ताकि फ़ाइलें registered assets शेयर करें।

initializer पर ध्यान दें: source code में Worker() और Worker(configuration:) मौजूद हैं। कुछ migration snippets में Worker.shared() दिखता है, जो Rive के example में एक app-level helper है, RiveRuntime का हिस्सा नहीं।

view model instance कैसे बनाएँ और उसे स्टेट मशीन से कैसे बाइंड करें?

फ़ाइल से instance बनाएँ, फिर उसे createStateMachine(_:binding:) में पास करें। .viewModelDefault(from: .artboardDefault(artboard)) का मतलब है "इस आर्टबोर्ड को असाइन किए गए view model का default instance"। दूसरे sources हैं .blank(from:) और नाम वाले instance के लिए .name(_:from:), और ViewModelSource.name("Character") नाम से व्यू मॉडल चुनता है।

setup को एक observable model में रखें ताकि instance, view body से ज़्यादा समय तक बना रहे:

import SwiftUI
import RiveRuntime

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

    private let mood = NumberProperty(path: "mood")

    func load() async {
        guard rive == nil else { return }
        do {
            let worker = try await Worker()
            let file = try await File(source: .local("character", Bundle.main), worker: worker)
            let artboard = try await file.createArtboard()
            let instance = try await file.createViewModelInstance(
                .viewModelDefault(from: .artboardDefault(artboard))
            )
            let stateMachine = try await artboard.createStateMachine(
                "CharacterViewModelMachine",
                binding: instance
            )
            self.instance = instance
            rive = try await Rive(file: file, artboard: artboard, stateMachine: stateMachine)
        } catch {
            print("Rive setup failed: \(error)")
        }
    }

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

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

struct CharacterView: View {
    @StateObject private var model = CharacterModel()

    var body: some View {
        RiveUIViewRepresentable(rive: model.rive)
            .frame(width: 240, height: 240)
            .accessibilityLabel("Animated character")
            .task { await model.load() }
    }
}

RiveUIViewRepresentable एक optional Rive लेता है, इसलिए loading पूरी होने से पहले भी view दिख सकता है। source में initializer पर label (rive:) है, भले ही कुछ doc snippets में यह label छूट गया हो। अगर आप loading को view के अंदर ही रखना चाहते हैं, तो AsyncRiveUIViewRepresentable { … } एक async closure लेता है जो Rive लौटाता है; तब instance को संभालकर रखने के लिए आपको कोई दूसरा तरीका चाहिए होगा।

जहाँ संभव हो, initial values view दिखाए जाने से पहले सेट करें। Rive की migration guide के अनुसार इससे ऐसा कोई फ़्रेम नहीं दिखता जिसमें फ़ाइल की default values हों।

SwiftUI से मूड नंबर कैसे सेट करें और ट्रिगर कैसे फ़ायर करें?

property सेट करने पर बदलाव queue हो जाता है और call तुरंत लौट आती है; legacy रनटाइम के उलट, इसके बाद आपको play() कॉल नहीं करना पड़ता। ट्रिगर की कोई value नहीं होती, इसलिए उन्हें फ़ायर किया जाता है:

struct UploadScreen: View {
    @StateObject private var character = CharacterModel()

    var body: some View {
        VStack(spacing: 24) {
            RiveUIViewRepresentable(rive: character.rive)
                .frame(width: 220, height: 220)
            Button("Upload") { Task { await upload() } }
        }
        .task { await character.load() }
    }

    private func upload() async {
        character.setMood(5)                       // loading loop
        let ok = await performUpload()
        character.fire(ok ? "success" : "error")   // one-shot reaction
        character.setMood(0)                       // back to idle
    }
}

transitions स्टेट मशीन संभालती है, इसलिए आपका code सिर्फ़ यह बताता है कि ऐप क्या कर रहा है। अच्छी तरह बनी फ़ाइल में ट्रिगर का वन-शॉट पूरा होता है और फिर कैरेक्टर उसी लूप पर लौट आता है जो mood इस समय चुन रहा है।

Charmotion फ़ाइलें ठीक यही ढाँचा इस्तेमाल करती हैं: CharacterViewModelMachine पर एक Character व्यू मॉडल, इन वैल्यू के साथ। हर पेड-प्लान सेट में इस रनटाइम के लिए एक तैयार {Name}NewRuntimeView.swift और पुराने RiveViewModel के लिए एक {Name}View.swift भी आता है।

moodलूपmoodलूप
0idle5loading
1happy6listening
2wave7talking
3thinking8sleeping
4sad9attention

ट्रिगर: success, error, yes, no, enter, exit।

एक कैरेक्टर की दस लूपिंग स्टेट, हर एक 0 से 9 तक के मूड नंबर से चुनी जाती है
एक Charmotion Rive file की दस लूपिंग स्टेट, जिन्हें मूड नंबर इनपुट से चुना जाता है।

Rive से वापस आने वाले property बदलाव को कैसे observe करें?

Data Binding दो-तरफ़ा है: स्टेट मशीन भी property बदल सकती है, जैसे जब यूज़र आर्टबोर्ड में किसी hit area पर टैप करे। String, number, boolean, color और enum properties valueStream(of:) देती हैं; ट्रिगर stream(of:) देते हैं, जो हर बार ट्रिगर फ़ायर होने पर Void emit करता है। एक बार पढ़ने के लिए try await instance.value(of:) इस्तेमाल करें।

extension CharacterModel {
    func watchMood() async {
        guard let instance else { return }
        do {
            for try await value in instance.valueStream(of: NumberProperty(path: "mood")) {
                print("mood is now \(Int(value))")
            }
        } catch {
            print("Stream ended: \(error)")
        }
    }

    func waitForSuccess() async throws {
        guard let instance else { return }
        for try await _ in instance.stream(of: TriggerProperty(path: "success")) {
            break   // first fire is enough
        }
    }
}

इन loops को .task से शुरू करें ताकि view गायब होने पर SwiftUI इन्हें cancel कर दे। Rive के docs के अनुसार throw होने वाला error ViewModelInstanceError है। redraw ट्रिगर करने के लिए deprecated dirtyStream() की ज़रूरत नहीं है; bound instances में बदलाव के बाद controller खुद redraw करता है।

नए रनटाइम में legacy RiveStateDelegate जैसा कोई state-name callback नहीं है। अगर आपके ऐप को यह जानना है कि "intro पूरा हो गया", तो इसे फ़ाइल में एक ट्रिगर या string property के रूप में बनाएँ और उसे observe करें।

मेरी binding अपडेट क्यों नहीं हो रही?

"कुछ नहीं हो रहा" वाली ज़्यादातर शिकायतों की वजह इनमें से कोई एक होती है:

  1. instance retain नहीं किया गया। अगर ViewModelInstance किसी function का local variable है, तो setup लौटते ही वह खत्म हो जाता है। Rive के docs: "binding के बाद जिन भी supplied instances को पढ़ना या बदलना है, उनके references retain करें। bound instances को स्टेट मशीन के ज़रिए वापस प्राप्त नहीं किया जा सकता।"
  2. आपने एक instance बाइंड किया और दूसरे पर values सेट कीं। createViewModelInstance से दूसरा instance बनाने पर आपको एक unbound copy मिलती है। उस पर values सेट करने से स्क्रीन पर कुछ नहीं बदलता।
  3. आपने Rive(file:) इस्तेमाल किया और फिर instance ढूँढा। convenience initializers editor में बने default instance को बाइंड करते हैं, जिसे आप वापस प्राप्त नहीं कर सकते; मौजूदा initializers के साथ deprecated rive.viewModelInstance getter nil लौटाता है।
  4. गलत path या type। paths case-sensitive होते हैं और nested properties में slash इस्तेमाल होता है ("settings/volume")। फ़ाइल में मौजूद number को NumberProperty से पढ़ना होगा, StringProperty से नहीं।
  5. गलत स्टेट मशीन नाम। नाम मौजूद न होने पर createStateMachine ArtboardError.invalidStateMachine throw करता है। इस error को चुपचाप नज़रअंदाज़ न करें।
  6. main actor के बाहर calls। ये APIs @MainActor हैं। ऊपर की तरह अपने model को @MainActor मार्क करें, फिर compiler इसे लागू करवाता है।

किसी अनजान फ़ाइल को जाँचने के लिए file.getViewModelNames(), file.getProperties(of:) और file.getDefaultViewModelInfo(for:) बताते हैं कि फ़ाइल में असल में क्या है।

नया रनटाइम किन platforms को सपोर्ट करता है?

दोनों रनटाइम एक ही पैकेज में आते हैं, इसलिए उनकी minimum versions भी एक जैसी हैं। Package.swift manifest में यह घोषित है:

platforms: [.iOS("14.0"), .visionOS("1.0"), .tvOS("16.0"), .macOS("13.1"), .macCatalyst("14.0")]

इसमें watchOS की कोई entry नहीं है, इसलिए Apple Watch को unsupported मानें, भले ही एक migration page के summary में कुछ और कहा गया हो। विकल्पों के लिए watchOS और visionOS पर कैरेक्टर एनिमेशन देखें।

view type RiveUIView है, जो Metal पर चलने वाला UIView है (macOS पर NSView)। RiveUIViewRepresentable इसे SwiftUI के लिए wrap करता है और modifiers जोड़ता है: .paused(_:), .frameRate(_:) (.default, .fps(30) या .range) और editor में बनाई गई VoiceOver semantics के लिए .semantics(_:), जिसे सिर्फ़ नया रनटाइम सपोर्ट करता है।

क्या मुझे अब भी RiveViewModel और स्टेट मशीन इनपुट इस्तेमाल करने चाहिए?

legacy रनटाइम तब इस्तेमाल करें जब आपकी फ़ाइल में सिर्फ़ इनपुट हों और आप उसे बदल न सकें, जब आप Rive events पर निर्भर हों, या जब आप किसी ऐसे deployment path को target कर रहे हों जिसे आप पहले ही ship और test कर चुके हैं। यह अब भी पैकेज में आता है, और वहाँ setInput और triggerInput अब भी काम करते हैं।

नए काम के लिए Rive नए रनटाइम को ही आगे की दिशा बताता है। migration का एक व्यावहारिक तरीका:

  1. editor में इनपुट को view models में बदलें, नाम वही रखें (mood, success)।
  2. RiveViewModel(fileName:stateMachineName:) की जगह Worker → File → Artboard → StateMachine → Rive chain इस्तेमाल करें।
  3. viewModel.view() की जगह RiveUIViewRepresentable(rive:) इस्तेमाल करें।
  4. setInput("mood", value:) की जगह setValue(of: NumberProperty(path: "mood"), to:) और triggerInput की जगह fire(trigger:) इस्तेमाल करें।
  5. pause() की जगह .paused(true) और setPreferredFramesPerSecond की जगह .frameRate(.fps(30)) इस्तेमाल करें।

UIKit screens में RiveUIView के साथ यही mapping लागू होती है; UIKit में Rive देखें।

SwiftUI में Rive view model बाइंड करें

  1. पैकेज जोड़ें. Swift Package Manager (6.24.0 या उसके बाद का version) से https://github.com/rive-app/rive-ios जोड़ें और अपनी .riv फ़ाइल को app target में bundle करें।
  2. फ़ाइल लोड करें. @MainActor model में try await Worker() और File(source: .local("character", Bundle.main), worker:) बनाएँ।
  3. instance बनाएँ और retain करें. file.createViewModelInstance(.viewModelDefault(from: .artboardDefault(artboard))) कॉल करें और नतीजे को एक property में स्टोर करें।
  4. बाइंड करें. artboard.createStateMachine("CharacterViewModelMachine", binding: instance) से स्टेट मशीन बनाएँ, फिर Rive(file:artboard:stateMachine:) बनाएँ।
  5. रेंडर करें. RiveUIViewRepresentable(rive: model.rive) को frame के साथ रखें और उसे .task से लोड करें।
  6. कंट्रोल करें. लूप के लिए setValue(of: NumberProperty(path: "mood"), to: Float(5)) और वन-शॉट के लिए fire(trigger: TriggerProperty(path: "success")) कॉल करें।

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

क्या नया Rive Apple रनटाइम setInput सपोर्ट करता है?

नहीं। स्टेट मशीन इनपुट सिर्फ़ legacy रनटाइम में हैं, और Rive के अनुसार इन्हें वापस लाने की कोई योजना नहीं है। editor में इनपुट को view model properties में बदलें, नाम वही रखें, और उनकी जगह bound view model instance पर NumberProperty, BoolProperty और TriggerProperty इस्तेमाल करें।

मेरा NumberProperty setValue, Int के साथ compile क्यों नहीं होता?

NumberProperty का value type Float है। Int variable पास करने पर type checking फ़ेल हो जाती है, इसलिए उसे कन्वर्ट करें: instance.setValue(of: NumberProperty(path: "mood"), to: Float(mood))। 5 जैसा literal काम करता है, क्योंकि Swift उसे Float मान लेता है। value(of:) या valueStream(of:) से value वापस पढ़ने पर भी Float ही मिलता है, इसलिए उसे enum में मैप करते समय Int(value) से कन्वर्ट करें।

क्या Rive(file:artboard:stateMachine:dataBind:) का इस्तेमाल अब भी ठीक है?

यह compile तो होता है, लेकिन deprecated है, और source में लिखा है कि इसे आने वाली किसी release में हटा दिया जाएगा। view model instance खुद बनाएँ और उसे संभालकर रखें, उसे artboard.createStateMachine(name, binding: instance) से bind करें, और फिर dataBind के बिना Rive initializer इस्तेमाल करें। इससे आपको वह instance reference भी मिल जाता है जो setValue और fire के लिए चाहिए।

क्या मैं कई Rive फ़ाइलों के लिए एक ही Worker इस्तेमाल कर सकता हूँ?

हाँ। एक ही Worker से बनी फ़ाइलें उसका background thread और उस पर रजिस्टर किए गए सभी out-of-band assets शेयर करती हैं। जब कई भारी ग्राफ़िक्स को अपना-अपना thread चाहिए हो, तो Rive अलग workers इस्तेमाल करने का सुझाव देता है। दोनों ही स्थितियों में, worker को तब तक बनाए रखें जब तक उसकी फ़ाइलें इस्तेमाल में हैं।

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

स्रोत

संबंधित गाइड