Add support for nested text runs on iOS

This adds support for getting / setting nested text runs on iOS, via the exposed lower-level API. Tests have been added for: testing that setting the text run works at the artboard level, and at the view model level, and additionally that the parent text run will be returned if the path is empty.

Diffs=
bbec5cbbc Add support for nested text runs on iOS (#8108)

Co-authored-by: David Skuza <david@rive.app>
This commit is contained in:
dskuza
2024-09-10 14:20:35 +00:00
parent b0474633b8
commit 2787cd9c6d
8 changed files with 119 additions and 5 deletions

View File

@@ -1 +1 @@
a4e15fb7b532b276e15b5d3b3a60843581641a05
bbec5cbbcce1ab111261066f885b0d46ecee4186

View File

@@ -94,6 +94,7 @@
E599DCF92AAFA06100D1E49A /* rating_animation.riv in Resources */ = {isa = PBXBuildFile; fileRef = E599DCF82AAFA06100D1E49A /* rating_animation.riv */; };
E599DCFA2AAFA06100D1E49A /* rating_animation.riv in Resources */ = {isa = PBXBuildFile; fileRef = E599DCF82AAFA06100D1E49A /* rating_animation.riv */; };
F21F08142C66526D00FFA205 /* RiveFallbackFontDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F21F08132C66526D00FFA205 /* RiveFallbackFontDescriptor.swift */; };
F23626AA2C8F90FA00727D9A /* nested_text_run.riv in Resources */ = {isa = PBXBuildFile; fileRef = F23626A92C8F90FA00727D9A /* nested_text_run.riv */; };
F28DE4532C5002D900F3C379 /* RiveModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F28DE4522C5002D900F3C379 /* RiveModelTests.swift */; };
F2D285492C6D469900728340 /* RiveFallbackFontProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2D285482C6D469900728340 /* RiveFallbackFontProvider.swift */; };
F2ECC2312C666824008B20E5 /* RiveFallbackFontDescriptor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2ECC2302C666824008B20E5 /* RiveFallbackFontDescriptor+Extensions.swift */; };
@@ -200,6 +201,7 @@
E5964A972A9697B600140479 /* RiveEvent.mm */ = {isa = PBXFileReference; explicitFileType = sourcecode.cpp.objcpp; path = RiveEvent.mm; sourceTree = "<group>"; };
E599DCF82AAFA06100D1E49A /* rating_animation.riv */ = {isa = PBXFileReference; lastKnownFileType = file; name = rating_animation.riv; path = "Example-iOS/Assets/rating_animation.riv"; sourceTree = SOURCE_ROOT; };
F21F08132C66526D00FFA205 /* RiveFallbackFontDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RiveFallbackFontDescriptor.swift; sourceTree = "<group>"; };
F23626A92C8F90FA00727D9A /* nested_text_run.riv */ = {isa = PBXFileReference; lastKnownFileType = file; path = nested_text_run.riv; sourceTree = "<group>"; };
F28DE4522C5002D900F3C379 /* RiveModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RiveModelTests.swift; sourceTree = "<group>"; };
F2D285482C6D469900728340 /* RiveFallbackFontProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RiveFallbackFontProvider.swift; sourceTree = "<group>"; };
F2ECC2302C666824008B20E5 /* RiveFallbackFontDescriptor+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RiveFallbackFontDescriptor+Extensions.swift"; sourceTree = "<group>"; };
@@ -260,6 +262,7 @@
04BE53F726493FE600427B39 /* Assets */ = {
isa = PBXGroup;
children = (
F23626A92C8F90FA00727D9A /* nested_text_run.riv */,
0424A8752BD59435000A9A1C /* img_test.riv */,
04E2223F2BE3C85100D82668 /* ball_test.riv */,
0424A8732BD592F2000A9A1C /* audio_test.riv */,
@@ -544,6 +547,7 @@
C38BB5F4287629C20039E385 /* defaultstatemachine.riv in Resources */,
04BE54082649403600427B39 /* flux_capacitor.riv in Resources */,
04BE54052649403600427B39 /* noartboard.riv in Resources */,
F23626AA2C8F90FA00727D9A /* nested_text_run.riv in Resources */,
0424A8742BD592F2000A9A1C /* audio_test.riv in Resources */,
0412652A2B0CCB8E009400EC /* embedded_assets.riv in Resources */,
04BE540B2649403600427B39 /* multiple_animations.riv in Resources */,

View File

@@ -243,6 +243,24 @@ static int artInstanceCount = 0;
return nullptr;
}
- (RiveTextValueRun*)textRun:(NSString*)name path:(NSString*)path
{
if (path.length == 0)
{
return [self textRun:name];
}
const std::string stdName = std::string([name UTF8String]);
const std::string stdPath = std::string([path UTF8String]);
// Can we update the cpp library to handle empty paths / default to parent if nullptr?
auto riveTextRun = _artboardInstance->getTextRun(stdName, stdPath);
if (riveTextRun != nullptr)
{
return [[RiveTextValueRun alloc] initWithTextValueRun:std::move(riveTextRun)];
}
return nullptr;
}
- (RiveSMIBool*)getBool:(NSString*)name path:(NSString*)path
{
// Create a unique dictionary name for nested artboards + booleans;

View File

@@ -46,6 +46,7 @@ NS_ASSUME_NONNULL_BEGIN
- (RiveStateMachineInstance* __nullable)defaultStateMachine;
- (RiveTextValueRun* __nullable)textRun:(NSString*)name;
- (RiveTextValueRun* __nullable)textRun:(NSString*)name path:(NSString*)path;
- (void)advanceBy:(double)elapsedSeconds;
- (void)draw:(RiveRenderer*)renderer;

View File

@@ -399,7 +399,19 @@ import Combine
}
return nil
}
/// Get a text value from a specified text run
/// - Parameters:
/// - textRunName: The name of a `Text Run` on the active Artboard
/// - path: The path to the nested text run.
/// - Returns: String text value of the specified text run if applicable
@objc open func getTextRunValue(_ textRunName: String, path: String) -> String? {
if let textRun = riveModel?.artboard?.textRun(textRunName, path: path) {
return textRun.text()
}
return nil
}
/// Set a text value for a specified text run
/// - Parameters:
/// - textRunName: The name of a `Text Run` on the active Artboard
@@ -407,11 +419,33 @@ import Combine
@objc open func setTextRunValue(_ textRunName: String, textValue: String) throws {
if let textRun = riveModel?.artboard?.textRun(textRunName) {
textRun.setText(textValue)
if isPlaying == false {
riveView?.advance(delta: 0)
}
} else {
throw RiveError.textValueRunError("Could not set text value on text run: \(textRunName) as the text run could not be found from the active artboard")
}
}
/// Set a text value for a specified text run
/// - Parameters:
/// - textRunName: The name of a `Text Run` on the active Artboard
/// - path: The path to the nested text run.
/// - value: A String value for the text run
/// - Note: If the specified path is empty, the parent artboard will be used to find the text run.
@objc open func setTextRunValue(_ textRunName: String, path: String, textValue: String) throws {
if let textRun = riveModel?.artboard?.textRun(textRunName, path: path) {
textRun.setText(textValue)
if isPlaying == false {
riveView?.advance(delta: 0)
}
} else {
throw RiveError.textValueRunError("Could not set text value on text run: \(textRunName) as the text run could not be found from the active artboard")
}
}
// TODO: Replace this with a more robust structure of the file's contents
@objc open func artboardNames() -> [String] {
return riveModel?.riveFile.artboardNames() ?? []

Binary file not shown.

View File

@@ -141,6 +141,27 @@
XCTAssertTrue([[textRun text] isEqualToString:@"Hello text"]);
}
/*
* Test setting a nested RiveTextValueRun text value
*/
- (void)testSettingNestedTextRunValue
{
RiveFile* file = [Util loadTestFile:@"nested_text_run" error:nil];
NSError* error = nil;
RiveArtboard* artboard = [file artboardFromName:@"Artboard" error:&error];
// If there is no path specified, check the parent artboard
RiveTextValueRun* textRun = [artboard textRun:@"parent" path:@""];
XCTAssertTrue([[textRun text] isEqualToString:@"Parent"]);
// Otherwise, test nested artboard naming
textRun = [artboard textRun:@"text" path:@"Nested/Two-Deep"];
XCTAssertTrue([[textRun text] isEqualToString:@"Text"]);
[textRun setText:@"Hello text"];
XCTAssertTrue([[textRun text] isEqualToString:@"Hello text"]);
}
- (void)testCatchingErrorOnBadTextRun
{
RiveFile* file = [Util loadTestFile:@"testtext" error:nil];

View File

@@ -23,13 +23,49 @@ class RiveViewModelTest: XCTestCase {
view.advance(delta: 0.1)
}
func testChangingTextRun() throws {
func testChangingTextRun_updatesText_andAdvances() throws {
let file = try RiveFile(testfileName: "testtext")
let model = RiveModel(riveFile: file)
let viewModel = RiveViewModel(model, autoPlay: false)
let delegate = PlayerDelegate()
let view = viewModel.createRiveView()
view.playerDelegate = delegate
XCTAssertEqual(viewModel.getTextRunValue("MyRun"), "Hello there")
try viewModel.setTextRunValue("MyRun", textValue: "Hello test")
XCTAssertEqual(viewModel.getTextRunValue("MyRun"), "Hello test")
XCTAssertTrue(delegate.didAdvance)
}
func testChangingNestedTextRun_updatesText_andAdvances() throws {
let file = try RiveFile(testfileName: "nested_text_run")
let model = RiveModel(riveFile: file)
let viewModel = RiveViewModel(model, autoPlay: false)
let view = viewModel.createRiveView()
let delegate = PlayerDelegate()
view.playerDelegate = delegate
XCTAssertEqual(viewModel.getTextRunValue("text", path: "Nested/Two-Deep"), "Text")
try viewModel.setTextRunValue("text", path: "Nested/Two-Deep", textValue: "Hello test")
XCTAssertEqual(viewModel.getTextRunValue("text", path: "Nested/Two-Deep"), "Hello test")
XCTAssertTrue(delegate.didAdvance)
}
}
private extension RiveViewModelTest {
class PlayerDelegate: NSObject, RivePlayerDelegate {
var didAdvance = false
func player(playedWithModel riveModel: RiveRuntime.RiveModel?) { }
func player(pausedWithModel riveModel: RiveRuntime.RiveModel?) { }
func player(loopedWithModel riveModel: RiveRuntime.RiveModel?, type: Int) { }
func player(stoppedWithModel riveModel: RiveRuntime.RiveModel?) { }
func player(didAdvanceby seconds: Double, riveModel: RiveModel?) {
didAdvance = true
}
}
}