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

UIKit में Rive: RiveUIView और Legacy RiveView

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

iOS & Swift गाइड का कवर इलस्ट्रेशन: UIKit में Rive: RiveUIView और Legacy RiveView, Charmotion के डेमो कैरेक्टर के साथ

UIKit में नया Rive रनटाइम RiveUIView के ज़रिए रेंडर होता है: Worker, File, artboard और bound state machine से एक Rive object बनाएँ, फिर उसे riveView.rive को असाइन करें और view को Auto Layout के साथ जोड़ें। legacy रनटाइम RiveViewModel(fileName:stateMachineName:) और createRiveView() इस्तेमाल करता है, और उसे setInput और triggerInput से चलाया जाता है।

मुख्य बातें

  • RiveUIView (नया रनटाइम) Metal पर चलने वाला एक UIView है; इसकी rive property view के स्क्रीन पर आने के बाद भी सेट की जा सकती है।
  • RiveView (legacy) RiveViewModel.createRiveView() से मिलता है; व्यू मॉडल को property के रूप में रखें, वरना view रिस्पॉन्ड करना बंद कर देता है।
  • नए रनटाइम में isPaused से pause करें और frameRate = .fps(30) से फ़्रेम सीमित करें; legacy में pause() और setPreferredFramesPerSecond इस्तेमाल होते हैं।
  • नए रनटाइम में मूड, संभालकर रखे गए view model instance पर एक NumberProperty होता है, और legacy रनटाइम में एक नंबर इनपुट।

UIViewController में Rive एनिमेशन कैसे जोड़ें?

नए रनटाइम में पहले ही rive: nil के साथ एक RiveUIView बनाएँ, उसका layout सेट करें, और loading पूरी होने पर Rive object असाइन करें। बाद में rive को फिर से सेट करने पर ग्राफ़िक बदल जाता है।

import UIKit
import RiveRuntime

final class WelcomeViewController: UIViewController {
    private let riveView = RiveUIView(rive: nil)
    private var character: ViewModelInstance?

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground

        riveView.translatesAutoresizingMaskIntoConstraints = false
        riveView.isAccessibilityElement = true
        riveView.accessibilityLabel = "Animated mascot"
        view.addSubview(riveView)
        NSLayoutConstraint.activate([
            riveView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            riveView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            riveView.widthAnchor.constraint(equalToConstant: 240),
            riveView.heightAnchor.constraint(equalTo: riveView.widthAnchor),
        ])

        Task { await loadCharacter() }
    }

    private func loadCharacter() async {
        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 machine = try await artboard.createStateMachine(
                "CharacterViewModelMachine",
                binding: instance
            )
            character = instance
            riveView.rive = try await Rive(file: file, artboard: artboard, stateMachine: machine)
        } catch {
            print("Rive failed to load: \(error)")
        }
    }
}

UIViewController के किसी method में बनाया गया Task main actor इनहेरिट करता है, और Rive के @MainActor APIs को यही चाहिए। अगर आपको instance की ज़रूरत नहीं है (सिर्फ़ सजावटी लूप), तो RiveUIView({ … }) ऐसा async closure भी लेता है जो Rive लौटाता है; Rive के docs इसे try await Rive(file: file) के साथ दिखाते हैं, जो फ़ाइल का default instance अपने-आप bind कर देता है।

RiveUIView और legacy RiveView में क्या अंतर है?

दोनों एक ही RiveRuntime module में UIView subclasses हैं, लेकिन ये अलग-अलग रनटाइम के हैं और एक ग्राफ़िक के लिए इन्हें मिलाकर इस्तेमाल नहीं किया जा सकता।

RiveUIView (नया)RiveView (legacy)
किससे बनता हैRiveUIView(rive:) या RiveUIView({ async closure })riveViewModel.createRiveView()
कंट्रोलरएक Rive object और आपका संभालकर रखा गया ViewModelInstanceवह RiveViewModel जिसने इसे बनाया
स्टेट बदलनाData binding properties और ट्रिगरsetInput, triggerInput (और legacy data binding)
ThreadingWorker का background thread, API main actor परMain thread
रोकेंisPaused = trueviewModel.pause()
Frame rateframeRate = .fps(30)setPreferredFramesPerSecond(preferredFramesPerSecond: 30)
Events, linear एनिमेशनसपोर्ट नहीं हैसपोर्ट है

फ़ाइल के हिसाब से चुनें: सिर्फ़ inputs वाली .riv के लिए legacy view चाहिए; view models वाली फ़ाइल दोनों के साथ काम करती है, और Rive नए काम के लिए RiveUIView की सलाह देता है।

RiveViewModel.createRiveView() से RiveView कैसे बनाएँ?

legacy रनटाइम में फ़ाइल और स्टेट मशीन व्यू मॉडल के पास होते हैं, और createRiveView() उससे जुड़ा एक RiveView लौटाता है। व्यू मॉडल को property के रूप में स्टोर करें: इनपुट इसी object को भेजे जाते हैं।

import UIKit
import RiveRuntime

final class InboxEmptyViewController: UIViewController {
    private let mascot = RiveViewModel(
        fileName: "character",
        stateMachineName: "CharacterStateMachine"
    )

    override func viewDidLoad() {
        super.viewDidLoad()
        let riveView = mascot.createRiveView()
        riveView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(riveView)
        NSLayoutConstraint.activate([
            riveView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            riveView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 48),
            riveView.widthAnchor.constraint(equalToConstant: 200),
            riveView.heightAnchor.constraint(equalToConstant: 200),
        ])
        mascot.setInput("mood", value: Float(8))   // sleeping: calm empty state
    }
}

fileName बिना .riv के bundle resource का नाम है। initializer fit (default .contain), alignment, autoPlay और artboardName भी लेता है। एक RiveViewModel एक ही view चलाता है; कैरेक्टर की हर दिखने वाली कॉपी के लिए अलग व्यू मॉडल बनाएँ।

table या collection view header में व्यू मॉडल को cell या header object के साथ बनाएँ, cellForRowAt में नहीं, ताकि scroll करते समय फ़ाइल बार-बार reload न हो।

legacy रनटाइम में setInput से मूड कैसे चलाएँ?

RiveViewModel typed overloads देता है: setInput(_:value:), जो Bool, Float और Double के लिए है, और triggerInput(_:), जो ट्रिगर के लिए है। इन्हें अपने view controller के state changes से जोड़ें:

extension InboxEmptyViewController {
    func refreshStarted() {
        mascot.setInput("mood", value: Float(5))     // loading
    }

    func refreshFinished(newItems: Int) {
        if newItems > 0 {
            mascot.triggerInput("success")
            mascot.setInput("mood", value: Float(1)) // happy
        } else {
            mascot.setInput("mood", value: Float(8)) // sleeping
        }
    }

    func refreshFailed() {
        mascot.triggerInput("error")
        mascot.setInput("mood", value: Float(4))     // sad
    }
}

इनपुट के नाम case-sensitive होते हैं और स्टेट मशीन से मेल खाने चाहिए। Rive इनपुट को legacy feature बताता है: ये इस रनटाइम में काम करते रहते हैं, लेकिन नई फ़ाइलों में इनकी जगह view models होने चाहिए। Charmotion फ़ाइलों में दोनों होते हैं, इसलिए वही .riv यहाँ CharacterStateMachine के ज़रिए और नए रनटाइम में CharacterViewModelMachine के ज़रिए काम करती है; generate की गई {Name}View.swift और {Name}NewRuntimeView.swift फ़ाइलें दोनों तरीके दिखाती हैं।

नए रनटाइम में UIKit से मूड कैसे सेट करें?

setup के दौरान जो view model instance आपने bind किया था (पहले उदाहरण में character property), उसे रखें और उसके typed setters कॉल करें। ये बदलाव को queue करके तुरंत लौट आते हैं; play() कॉल करने की ज़रूरत नहीं है।

extension WelcomeViewController {
    private static let mood = NumberProperty(path: "mood")

    func showListening() {
        character?.setValue(of: Self.mood, to: Float(6))
    }

    func celebrate() {
        character?.fire(trigger: TriggerProperty(path: "success"))
    }

    func observeTaps() {
        guard let character else { return }
        Task {
            for try await _ in character.stream(of: TriggerProperty(path: "yes")) {
                print("The user tapped yes inside the artboard")
            }
        }
    }
}

stream(of:) हर बार emit करता है जब कोई ट्रिगर फ़ायर होता है, तब भी जब फ़ाइल उसे किसी listener से फ़ायर करती है। controller हटने पर task को cancel करें, जैसे उसे स्टोर करके viewDidDisappear में cancel() कॉल करें।

UIKit में Rive को pause कैसे करें या उसका frame rate कैसे सीमित करें?

बैटरी बचाने के लिए स्क्रीन से बाहर वाले या सजावटी एनिमेशन को pause करें, और तब भी जब Reduce Motion (मोशन कम करें) चालू हो।

// New runtime
riveView.isPaused = true
riveView.frameRate = .fps(30)
riveView.frameRate = .range(minimum: 30, maximum: 60, preferred: nil)
riveView.frameRate = .default

// Legacy runtime
mascot.pause()
mascot.play()
mascot.setPreferredFramesPerSecond(preferredFramesPerSecond: 30)

Rive बताता है कि iOS 15 से पुराने versions पर frame-rate range एक तय .fps value की तरह काम करती है। छोटे मैस्कॉट के लिए अच्छा default यह है कि viewDidDisappear में pause करें और viewWillAppear में फिर से शुरू करें। स्क्रीन खुली रहते हुए Reduce Motion के बदलावों पर रिएक्ट करने के लिए SwiftUI और UIKit में Reduce Motion देखें।

UIKit स्क्रीन को RiveViewModel से नए रनटाइम पर कैसे माइग्रेट करें?

Rive की migration गाइड हर legacy कॉल को उसकी नई कॉल से मैप करती है:

Legacyनया रनटाइम
RiveViewModel(fileName:stateMachineName:)Worker() → File(source:worker:) → createArtboard() → createStateMachine(_:binding:) → Rive(file:artboard:stateMachine:)
createRiveView()RiveUIView(rive:)
viewModel.view() (SwiftUI)RiveUIViewRepresentable(rive:)
setInput("mood", value:)setValue(of: NumberProperty(path: "mood"), to:)
triggerInput("success")fire(trigger: TriggerProperty(path: "success"))
pause() / play()isPaused = true / false
setPreferredFramesPerSecond(preferredFramesPerSecond: 30)frameRate = .fps(30)
State-change delegatevalueStream(of:) से property को observe करें

एक बार में एक स्क्रीन माइग्रेट करें: दोनों रनटाइम एक ही ऐप और package में साथ चल सकते हैं। पहले पक्का करें कि फ़ाइल में view models हैं (editor का Convert Inputs to ViewModels इसमें मदद करता है), फिर view बदलें, फिर कॉल। इसी बदलाव का SwiftUI वाला हिस्सा SwiftUI में Rive data binding में बताया गया है।

UIKit view controller में Rive कैरेक्टर दिखाएँ (नया रनटाइम)

  1. View जोड़ें. RiveUIView(rive: nil) बनाएँ, उसे subview के रूप में जोड़ें और Auto Layout से उसका साइज़ तय करें।
  2. Task में लोड करें. viewDidLoad से Task { await loadCharacter() } शुरू करें, ताकि async setup main actor पर चले।
  3. View model instance bind करें. file.createViewModelInstance से instance बनाएँ, उसे स्टोर करें, और artboard.createStateMachine(_:binding:) को पास करें।
  4. Rive object असाइन करें. riveView.rive = try await Rive(file:artboard:stateMachine:) सेट करें।
  5. चलाएँ और pause करें. स्टोर किए गए instance पर setValue(of:to:) और fire(trigger:) कॉल करें; स्क्रीन छिपने पर isPaused सेट करें।

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

मेरा legacy RiveView setInput पर रिस्पॉन्ड करना क्यों बंद कर देता है?

आमतौर पर RiveViewModel एक local variable था और viewDidLoad के बाद release हो गया, या उसी view के लिए दूसरा व्यू मॉडल बना दिया गया। व्यू मॉडल को view controller की property के रूप में स्टोर करें, उससे view सिर्फ़ एक बार बनाएँ, और हर इनपुट उसी object को भेजें।

क्या मैं UITableViewCell में RiveUIView इस्तेमाल कर सकता हूँ?

हाँ, लेकिन cellForRowAt में फ़ाइल लोड करने से बचें। File को एक बार लोड करें, उसे shared model में रखें, और हर दिखने वाले cell के लिए एक Rive object बनाएँ, या list में static image दिखाएँ और live RiveUIView सिर्फ़ detail screen में। जो views scroll होकर स्क्रीन से बाहर जाएँ, उन्हें pause करें।

क्या RiveUIView macOS और visionOS पर काम करता है?

RiveUIView macOS पर NSView है और बाकी जगह UIView, और package में iOS 14 के साथ macOS 13.1, Mac Catalyst 14, tvOS 16 और visionOS 1 घोषित हैं। package manifest में watchOS platform नहीं है, इसलिए Apple Watch ऐप्स के लिए कोई दूसरा तरीका अपनाना होगा।

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

स्रोत

संबंधित गाइड