-
Notifications
You must be signed in to change notification settings - Fork 97
Description
Description
When using the @_spi(RiveExperimental) API, setting a ViewModel string property to an empty string ("") causes valueStream to throw a missingData error. This terminates the AsyncThrowingStream permanently — the listener is killed and cannot receive any further updates.
An empty string is a valid value and should be emitted through the stream, not treated as missing data.
Root cause
The bug originates in the ObjC++ bridge at RiveViewModelInstanceData.mm:90:
case rive::DataType::string:
case rive::DataType::enumType:
if (!data.stringValue.empty()) // ← filters out empty strings
{
_stringValue = [NSString stringWithUTF8String:data.stringValue.c_str()];
}
break;When the C++ runtime delivers an empty string, data.stringValue.empty() returns true, so _stringValue is never set and stays nil.
On the Swift side, ViewModelInstanceService.swift:671-684 checks data.stringValue, which is nil, so all type checks fail and it falls through to:
streamContinuation.finish(throwing: ViewModelInstanceError.missingData)
streamContinuations.removeValue(forKey: requestID)This kills the stream permanently.
Provide a Repro
@_spi(RiveExperimental) import RiveRuntime
let nameProp = StringProperty(path: "childVm/name")
// Start listening
Task { @MainActor in
let stream = vmi.valueStream(of: nameProp)
do {
for try await val in stream {
print("Value: '\(val)'")
}
} catch {
// THIS IS HIT: "missingData"
print("Stream error: \(error)")
}
}
// Set to empty string — stream throws missingData and terminates
vmi.setValue(of: nameProp, to: "")Unit tests reproducing the bug
// In ViewModelInstanceTests.swift
@MainActor
func test_valueStream_withStringProperty_throwsMissingDataOnEmptyString() async throws {
let mockCommandQueue = MockCommandQueue()
let viewModelInstance = makeViewModelInstance(mockCommandQueue: mockCommandQueue)
let property = StringProperty(path: "childVm/name")
let stream = viewModelInstance.valueStream(of: property)
let subscribeCall = mockCommandQueue.subscribeToViewModelPropertyCalls[0]
let observer = mockCommandQueue.getObserver(for: 99)
let requestID = subscribeCall.requestID
let errorExpectation = expectation(description: "Stream throws missingData")
let task = Task {
do {
for try await _ in stream {}
XCTFail("Stream should have thrown, not ended normally")
} catch let error as ViewModelInstanceError {
guard case .missingData = error else {
XCTFail("Expected missingData, got \(error)")
return
}
errorExpectation.fulfill()
}
}
// Simulate what happens when the ObjC++ bridge receives an empty string:
// RiveViewModelInstanceData.mm filters out empty strings via `if (!data.stringValue.empty())`
// which leaves _stringValue as nil.
let emptyStringData = MockRiveViewModelInstanceData(stringValue: nil)
observer?.onViewModelDataReceived(99, requestID: requestID, data: emptyStringData)
await fulfillment(of: [errorExpectation], timeout: 1.0)
task.cancel()
}
@MainActor
func test_value_withStringProperty_throwsMissingDataOnEmptyString() async throws {
let mockCommandQueue = MockCommandQueue()
var capturedObserver: ViewModelInstanceListener?
let viewModelInstance = makeViewModelInstance(mockCommandQueue: mockCommandQueue) { observer in
capturedObserver = observer
}
mockCommandQueue.stubRequestViewModelInstanceString { instanceHandle, path, requestID in
let mockData = MockRiveViewModelInstanceData(stringValue: nil)
capturedObserver?.onViewModelDataReceived(instanceHandle, requestID: requestID, data: mockData)
}
let property = StringProperty(path: "childVm/name")
do {
_ = try await viewModelInstance.value(of: property)
XCTFail("Expected ViewModelInstanceError.missingData to be thrown")
} catch let error as ViewModelInstanceError {
guard case .missingData = error else {
XCTFail("Expected missingData error, got \(error)")
return
}
}
}Source .riv/.rev file
Any .riv file with a ViewModel containing a string property will reproduce this. Nested paths (e.g. "childVm/name") are confirmed affected.
Expected behavior
Setting a string property to "" should emit "" through the stream. An empty string is a valid value.
Suggested fix
In RiveViewModelInstanceData.mm, always set _stringValue for string/enum types:
case rive::DataType::string:
case rive::DataType::enumType:
_stringValue = [NSString stringWithUTF8String:data.stringValue.c_str()];
break;[NSString stringWithUTF8String:""] correctly produces @"" (non-nil empty NSString).
Device & Versions (please complete the following information)
- Device: iOS Simulator (iPhone 16)
- iOS version: iOS 18.2
- rive-ios: latest main (
397b290) - API:
@_spi(RiveExperimental)
Additional context
- The same bug likely affects
EnumPropertysince it shares the same code path - The one-shot
value(of:)API also returnsmissingDatafor empty strings (same root cause) - Current workaround is wrapping stream iteration in a retry loop, but this introduces gaps where updates can be missed