diff --git a/.rive_head b/.rive_head index e27c72d..a5ac792 100644 --- a/.rive_head +++ b/.rive_head @@ -1 +1 @@ -052a4984ef8526de04a111ea4e7b725271e53d38 +8f30ede7be2d34dd642dc6f53575f1ab9bd3525e diff --git a/Example-iOS/Assets/data_binding_test.riv b/Example-iOS/Assets/data_binding_test.riv index 1def8ad..289daf1 100644 Binary files a/Example-iOS/Assets/data_binding_test.riv and b/Example-iOS/Assets/data_binding_test.riv differ diff --git a/Example-iOS/Source/Examples/SwiftUI/DataBindingView.swift b/Example-iOS/Source/Examples/SwiftUI/DataBindingView.swift index 38384d1..dfee993 100644 --- a/Example-iOS/Source/Examples/SwiftUI/DataBindingView.swift +++ b/Example-iOS/Source/Examples/SwiftUI/DataBindingView.swift @@ -43,6 +43,10 @@ private class DataBindingViewModel: RiveViewModel { return dataBindingInstance?.viewModelInstanceProperty(fromPath: "Nested") } + var imageProperty: RiveDataBindingViewModel.Instance.ImageProperty? { + return dataBindingInstance?.imageProperty(fromPath: "Image") + } + init(fileName: String) { super.init(fileName: fileName) @@ -91,6 +95,7 @@ struct DataBindingView: DismissableView { updateEnum() updateNestedViewModel() updateTrigger() + updateImage() // Manually advance the Rive view since it is not playing. // When a Rive view is playing, this is handled for you. @@ -175,5 +180,18 @@ struct DataBindingView: DismissableView { guard let property = riveViewModel.triggerProperty(name: trigger) else { return } property.trigger() } + + private func updateImage() { + let images = [ + UIImage(systemName: "square.and.arrow.down")!, + UIImage(systemName: "paperplane")!, + UIImage(systemName: "externaldrive")!, + // or any other UIImage initializer + ] + guard let property = riveViewModel.imageProperty else { return } + let image = images.randomElement()! + guard let renderImage = RiveRenderImage(image: image, format: .png) else { return } + property.setValue(renderImage) + } } diff --git a/RiveRuntime.xcodeproj/project.pbxproj b/RiveRuntime.xcodeproj/project.pbxproj index 8147711..ee9b7c3 100644 --- a/RiveRuntime.xcodeproj/project.pbxproj +++ b/RiveRuntime.xcodeproj/project.pbxproj @@ -93,12 +93,17 @@ E5964A982A9697B600140479 /* RiveEvent.mm in Sources */ = {isa = PBXBuildFile; fileRef = E5964A972A9697B600140479 /* RiveEvent.mm */; }; 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 */; }; + F21C3D1B2DDFCD93005F82F4 /* RiveRenderImage+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F21C3D1A2DDFCD93005F82F4 /* RiveRenderImage+Extensions.swift */; }; F21F08142C66526D00FFA205 /* RiveFallbackFontDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F21F08132C66526D00FFA205 /* RiveFallbackFontDescriptor.swift */; }; F22CF1B12D380E3700D35779 /* data_binding_test.riv in Resources */ = {isa = PBXBuildFile; fileRef = F22CF1B02D380E3700D35779 /* data_binding_test.riv */; }; F22CF1B32D380E6900D35779 /* DataBindingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F22CF1B22D380E6900D35779 /* DataBindingTests.swift */; }; F22E12112D67D47C00F2E8C3 /* RiveDisplayLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = F23B2B7F2D2DCD70007AA53E /* RiveDisplayLink.swift */; }; F23626AA2C8F90FA00727D9A /* nested_text_run.riv in Resources */ = {isa = PBXBuildFile; fileRef = F23626A92C8F90FA00727D9A /* nested_text_run.riv */; }; F23992E72CB9C1C60021EF61 /* RenderContextTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F23992E62CB9C1C60021EF61 /* RenderContextTests.m */; }; + F24FC6452DD3C83700DEE8C5 /* RiveRenderImageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F24FC6442DD3C83700DEE8C5 /* RiveRenderImageTests.swift */; }; + F24FC6492DD3C85E00DEE8C5 /* 1x1_jpg.jpg in Resources */ = {isa = PBXBuildFile; fileRef = F24FC6472DD3C85E00DEE8C5 /* 1x1_jpg.jpg */; }; + F24FC64A2DD3C85E00DEE8C5 /* 1x1_png.png in Resources */ = {isa = PBXBuildFile; fileRef = F24FC6482DD3C85E00DEE8C5 /* 1x1_png.png */; }; + F24FC6572DD3D7AD00DEE8C5 /* data_binding_image_test.riv in Resources */ = {isa = PBXBuildFile; fileRef = F24FC6562DD3D7AD00DEE8C5 /* data_binding_image_test.riv */; }; F259E5872D35B91700B78FEF /* RiveDataBindingViewModel.mm in Sources */ = {isa = PBXBuildFile; fileRef = F259E5862D35B91700B78FEF /* RiveDataBindingViewModel.mm */; }; F259E5882D35B91700B78FEF /* RiveDataBindingViewModel.h in Headers */ = {isa = PBXBuildFile; fileRef = F259E5852D35B91700B78FEF /* RiveDataBindingViewModel.h */; settings = {ATTRIBUTES = (Public, ); }; }; F25C58A82D5A60EC00186C91 /* RiveLogger+DataBinding.swift in Sources */ = {isa = PBXBuildFile; fileRef = F25C58A72D5A60EC00186C91 /* RiveLogger+DataBinding.swift */; }; @@ -228,12 +233,17 @@ E5964A952A965A9300140479 /* RiveEvent.h */ = {isa = PBXFileReference; explicitFileType = sourcecode.cpp.h; path = RiveEvent.h; sourceTree = ""; }; E5964A972A9697B600140479 /* RiveEvent.mm */ = {isa = PBXFileReference; explicitFileType = sourcecode.cpp.objcpp; path = RiveEvent.mm; sourceTree = ""; }; E599DCF82AAFA06100D1E49A /* rating_animation.riv */ = {isa = PBXFileReference; lastKnownFileType = file; name = rating_animation.riv; path = "Example-iOS/Assets/rating_animation.riv"; sourceTree = SOURCE_ROOT; }; + F21C3D1A2DDFCD93005F82F4 /* RiveRenderImage+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RiveRenderImage+Extensions.swift"; sourceTree = ""; }; F21F08132C66526D00FFA205 /* RiveFallbackFontDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RiveFallbackFontDescriptor.swift; sourceTree = ""; }; F22CF1B02D380E3700D35779 /* data_binding_test.riv */ = {isa = PBXFileReference; lastKnownFileType = file; path = data_binding_test.riv; sourceTree = ""; }; F22CF1B22D380E6900D35779 /* DataBindingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBindingTests.swift; sourceTree = ""; }; F23626A92C8F90FA00727D9A /* nested_text_run.riv */ = {isa = PBXFileReference; lastKnownFileType = file; path = nested_text_run.riv; sourceTree = ""; }; F23992E62CB9C1C60021EF61 /* RenderContextTests.m */ = {isa = PBXFileReference; explicitFileType = sourcecode.cpp.objcpp; path = RenderContextTests.m; sourceTree = ""; }; F23B2B7F2D2DCD70007AA53E /* RiveDisplayLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RiveDisplayLink.swift; sourceTree = ""; }; + F24FC6442DD3C83700DEE8C5 /* RiveRenderImageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RiveRenderImageTests.swift; sourceTree = ""; }; + F24FC6472DD3C85E00DEE8C5 /* 1x1_jpg.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = 1x1_jpg.jpg; sourceTree = ""; }; + F24FC6482DD3C85E00DEE8C5 /* 1x1_png.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = 1x1_png.png; sourceTree = ""; }; + F24FC6562DD3D7AD00DEE8C5 /* data_binding_image_test.riv */ = {isa = PBXFileReference; lastKnownFileType = file; path = data_binding_image_test.riv; sourceTree = ""; }; F259E5852D35B91700B78FEF /* RiveDataBindingViewModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RiveDataBindingViewModel.h; sourceTree = ""; }; F259E5862D35B91700B78FEF /* RiveDataBindingViewModel.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = RiveDataBindingViewModel.mm; sourceTree = ""; }; F25C58A72D5A60EC00186C91 /* RiveLogger+DataBinding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RiveLogger+DataBinding.swift"; sourceTree = ""; }; @@ -321,6 +331,9 @@ 04BE53F726493FE600427B39 /* Assets */ = { isa = PBXGroup; children = ( + F24FC6562DD3D7AD00DEE8C5 /* data_binding_image_test.riv */, + F24FC6472DD3C85E00DEE8C5 /* 1x1_jpg.jpg */, + F24FC6482DD3C85E00DEE8C5 /* 1x1_png.png */, F22CF1B02D380E3700D35779 /* data_binding_test.riv */, F2CCA9782C9B2799007DC0D2 /* referenced_image_asset.riv */, F23626A92C8F90FA00727D9A /* nested_text_run.riv */, @@ -429,6 +442,7 @@ C9C73ED324FC478800EF9516 /* Source */ = { isa = PBXGroup; children = ( + F24FC6502DD3CF6700DEE8C5 /* Extensions */, F259E5842D35B87800B78FEF /* DataBinding */, F2CCA9C02C9E13B2007DC0D2 /* Logging */, C3468E5727EB9887008652FD /* RiveView.swift */, @@ -466,6 +480,7 @@ F2ECC2382C66B920008B20E5 /* RiveFontTests.swift */, F23992E62CB9C1C60021EF61 /* RenderContextTests.m */, F22CF1B22D380E6900D35779 /* DataBindingTests.swift */, + F24FC6442DD3C83700DEE8C5 /* RiveRenderImageTests.swift */, ); path = Tests; sourceTree = ""; @@ -484,6 +499,14 @@ path = Fonts; sourceTree = ""; }; + F24FC6502DD3CF6700DEE8C5 /* Extensions */ = { + isa = PBXGroup; + children = ( + F21C3D1A2DDFCD93005F82F4 /* RiveRenderImage+Extensions.swift */, + ); + path = Extensions; + sourceTree = ""; + }; F259E5842D35B87800B78FEF /* DataBinding */ = { isa = PBXGroup; children = ( @@ -673,6 +696,7 @@ 0424A8742BD592F2000A9A1C /* audio_test.riv in Resources */, 0412652A2B0CCB8E009400EC /* embedded_assets.riv in Resources */, 04BE540B2649403600427B39 /* multiple_animations.riv in Resources */, + F24FC6572DD3D7AD00DEE8C5 /* data_binding_image_test.riv in Resources */, 041265282B0CC387009400EC /* hosted_assets.riv in Resources */, E57798AB2A7310B700FF25C3 /* testtext.riv in Resources */, 04ED72F3299C115100E8DE53 /* empty_animation_state.riv in Resources */, @@ -684,6 +708,8 @@ 04BE54092649403600427B39 /* state_machine_configurations.riv in Resources */, 04BE54072649403600427B39 /* what_a_state.riv in Resources */, 04E222402BE3C85100D82668 /* ball_test.riv in Resources */, + F24FC6492DD3C85E00DEE8C5 /* 1x1_jpg.jpg in Resources */, + F24FC64A2DD3C85E00DEE8C5 /* 1x1_png.png in Resources */, 0424A8762BD59435000A9A1C /* img_test.riv in Resources */, 04BE541A264A823000427B39 /* animationconfigurations.riv in Resources */, 04BE540E2649403600427B39 /* multiple_state_machines.riv in Resources */, @@ -733,6 +759,7 @@ 046FB7F8264EAA60000129B1 /* RiveStateMachineInstance.mm in Sources */, C3468E5C27ED4C41008652FD /* RiveModel.swift in Sources */, F259E5872D35B91700B78FEF /* RiveDataBindingViewModel.mm in Sources */, + F21C3D1B2DDFCD93005F82F4 /* RiveRenderImage+Extensions.swift in Sources */, 046FB7FF264EAA61000129B1 /* RiveFile.mm in Sources */, F2FD94082CC94B1A00C1FC85 /* RiveFallbackFontCache.m in Sources */, 046FB7F2264EAA60000129B1 /* RiveArtboard.mm in Sources */, @@ -763,6 +790,7 @@ F22CF1B32D380E6900D35779 /* DataBindingTests.swift in Sources */, 04BE5418264A818F00427B39 /* RiveAnimationConfigurationsTest.mm in Sources */, 04BE5427264C02AA00427B39 /* StateMachineInstanceTest.mm in Sources */, + F24FC6452DD3C83700DEE8C5 /* RiveRenderImageTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Source/DataBinding/RiveDataBindingViewModelInstance.h b/Source/DataBinding/RiveDataBindingViewModelInstance.h index e3c671b..2e57f93 100644 --- a/Source/DataBinding/RiveDataBindingViewModelInstance.h +++ b/Source/DataBinding/RiveDataBindingViewModelInstance.h @@ -17,6 +17,7 @@ NS_ASSUME_NONNULL_BEGIN @class RiveDataBindingViewModelInstanceColorProperty; @class RiveDataBindingViewModelInstanceEnumProperty; @class RiveDataBindingViewModelInstanceTriggerProperty; +@class RiveDataBindingViewModelInstanceImageProperty; @class RiveDataBindingViewModelInstancePropertyData; /// An object that represents an instance of a view model, used to update @@ -159,6 +160,19 @@ NS_SWIFT_NAME(RiveDataBindingViewModel.Instance) - (nullable RiveDataBindingViewModelInstanceTriggerProperty*) triggerPropertyFromPath:(NSString*)path; +/// Gets an image property in the view model instance. +/// +/// - Note: Unlike a `RiveViewModel.Instance`, a strong reference to this type +/// does not have to be made. If the property exists, the underlying property +/// will be cached, and calling this function again with the same path is +/// guaranteed to return the same object. +/// +/// - Parameter path: The path to the number property. +/// +/// - Returns: The property if it exists at the supplied path, otherwise nil. +- (nullable RiveDataBindingViewModelInstanceImageProperty*) + imagePropertyFromPath:(NSString*)path; + /// Calls all registered property listeners for the properties of the view model /// instance. - (void)updateListeners; diff --git a/Source/DataBinding/RiveDataBindingViewModelInstance.mm b/Source/DataBinding/RiveDataBindingViewModelInstance.mm index 7af0c9e..8f2fc5e 100644 --- a/Source/DataBinding/RiveDataBindingViewModelInstance.mm +++ b/Source/DataBinding/RiveDataBindingViewModelInstance.mm @@ -312,6 +312,41 @@ return triggerProperty; } +- (RiveDataBindingViewModelInstanceImageProperty*)imagePropertyFromPath: + (NSString*)path +{ + RiveDataBindingViewModelInstanceImageProperty* cached; + if ((cached = [self + cachedPropertyFromPath:path + asClass: + [RiveDataBindingViewModelInstanceImageProperty + class]])) + { + return cached; + } + + auto image = _instance->propertyImage(std::string([path UTF8String])); + if (image == nullptr) + { + [RiveLogger logWithViewModelInstance:self + imagePropertyAtPath:path + found:NO]; + return nil; + } + + [RiveLogger logWithViewModelInstance:self + imagePropertyAtPath:path + found:YES]; + RiveDataBindingViewModelInstanceImageProperty* imageProperty = + [[RiveDataBindingViewModelInstanceImageProperty alloc] + initWithImage:image]; + imageProperty.valueDelegate = self; + + [self cacheProperty:imageProperty withPath:path]; + + return imageProperty; +} + - (void)updateListeners { [_properties enumerateKeysAndObjectsUsingBlock:^( diff --git a/Source/DataBinding/RiveDataBindingViewModelInstanceProperty.h b/Source/DataBinding/RiveDataBindingViewModelInstanceProperty.h index c0ab53d..05ae051 100644 --- a/Source/DataBinding/RiveDataBindingViewModelInstanceProperty.h +++ b/Source/DataBinding/RiveDataBindingViewModelInstanceProperty.h @@ -262,4 +262,33 @@ NS_SWIFT_NAME(RiveDataBindingViewModelInstance.TriggerProperty) @end +#pragma mark - Image + +typedef void (^RiveDataBindingViewModelInstanceImagePropertyListener)(void) + NS_SWIFT_NAME(RiveDataBindingViewModelInstanceImageProperty.Listener); + +/// An object that represents a trigger property of a view model instance. +NS_SWIFT_NAME(RiveDataBindingViewModelInstance.ImageProperty) +@interface RiveDataBindingViewModelInstanceImageProperty + : RiveDataBindingViewModelInstanceProperty + +- (instancetype)init NS_UNAVAILABLE; + +- (void)setValue:(nonnull RiveRenderImage*)image; + +/// Adds a block as a listener, called when the property is triggered. +/// +/// - Note: The property can be triggered either explicitly by the developer, +/// or as a result of a change in a state machine. +/// +/// - Parameter listener: The block that will be called when the property's +/// value changes. +/// +/// - Returns: A UUID for the listener, used in conjunction with +/// `removeListener`. +- (NSUUID*)addListener: + (RiveDataBindingViewModelInstanceImagePropertyListener)listener; + +@end + NS_ASSUME_NONNULL_END diff --git a/Source/DataBinding/RiveDataBindingViewModelInstanceProperty.mm b/Source/DataBinding/RiveDataBindingViewModelInstanceProperty.mm index b79900d..2753f90 100644 --- a/Source/DataBinding/RiveDataBindingViewModelInstanceProperty.mm +++ b/Source/DataBinding/RiveDataBindingViewModelInstanceProperty.mm @@ -536,3 +536,42 @@ } @end + +#pragma mark - Image + +@implementation RiveDataBindingViewModelInstanceImageProperty +{ + rive::ViewModelInstanceAssetImageRuntime* _image; +} + +- (instancetype)initWithImage:(rive::ViewModelInstanceAssetImageRuntime*)image +{ + if (self = [super initWithValue:image]) + { + _image = image; + } + return self; +} + +- (void)setValue:(RiveRenderImage*)renderImage +{ + [RiveLogger logPropertyUpdated:self value:@"new image"]; + _image->value([renderImage instance].get()); +} + +- (NSUUID*)addListener: + (RiveDataBindingViewModelInstanceTriggerPropertyListener)listener +{ + return [super addListener:listener]; +} + +- (void)handleListeners +{ + for (RiveDataBindingViewModelInstanceImagePropertyListener listener in self + .listeners.allValues) + { + listener(); + } +} + +@end diff --git a/Source/Extensions/RiveRenderImage+Extensions.swift b/Source/Extensions/RiveRenderImage+Extensions.swift new file mode 100644 index 0000000..9c24778 --- /dev/null +++ b/Source/Extensions/RiveRenderImage+Extensions.swift @@ -0,0 +1,26 @@ +// +// RiveRenderImage+Extensions.swift +// RiveRuntime +// +// Created by David Skuza on 5/22/25. +// Copyright © 2025 Rive. All rights reserved. +// + +public extension RiveRenderImage { + enum Format { + case jpeg(compressionQuality: CGFloat) + case png + } + + convenience init?(image: UIImage, format: Format) { + let data: Data? + switch format { + case .jpeg(let compressionQuality): + data = image.jpegData(compressionQuality: compressionQuality) + case .png: + data = image.pngData() + } + guard let data else { return nil } + self.init(data: data) + } +} diff --git a/Source/Logging/RiveLogger+DataBinding.swift b/Source/Logging/RiveLogger+DataBinding.swift index 2e17864..b53446f 100644 --- a/Source/Logging/RiveLogger+DataBinding.swift +++ b/Source/Logging/RiveLogger+DataBinding.swift @@ -27,6 +27,7 @@ enum RiveLoggerDataBindingEvent { case enumProperty(String, Bool) case viewModelProperty(String, Bool) case triggerProperty(String, Bool) + case imageProperty(String, Bool) } enum Property: DataBindingEvent { case propertyUpdated(String, String) @@ -106,6 +107,11 @@ extension RiveLogger { let message = message(instance: instance, for: "trigger", path: path, found: found) dataBinding.debug("\(message)") } + case .imageProperty(let path, let found): + _log(event: event, level: .debug) { + let message = message(instance: instance, for: "image", path: path, found: found) + dataBinding.debug("\(message)") + } } } @@ -174,6 +180,10 @@ extension RiveLogger { Self.log(viewModelInstance: instance, event: .triggerProperty(path, found)) } + @objc static func log(viewModelInstance instance: RiveDataBindingViewModel.Instance, imagePropertyAtPath path: String, found: Bool) { + Self.log(viewModelInstance: instance, event: .imageProperty(path, found)) + } + // MARK: - Properties @objc(logPropertyUpdated:value:) static func log(propertyUpdated property: RiveDataBindingViewModel.Instance.Property, value: String) { diff --git a/Source/Renderer/RiveFactory.mm b/Source/Renderer/RiveFactory.mm index d9ff70f..a7b2f51 100644 --- a/Source/Renderer/RiveFactory.mm +++ b/Source/Renderer/RiveFactory.mm @@ -12,6 +12,7 @@ #import #import #import +#import #if TARGET_OS_IPHONE #import @@ -41,6 +42,7 @@ static rive::rcp riveFontFromNativeFont(id font, rive::rcp instance; // note: we do NOT own this, so don't delete it } + - (instancetype)initWithImage:(rive::rcp)image { if (self = [super init]) @@ -53,6 +55,21 @@ static rive::rcp riveFontFromNativeFont(id font, return nil; } } + +- (instancetype)initWithData:(NSData*)data +{ + RenderContext* context = [[RenderContextManager shared] newDefaultContext]; + RiveFactory* factory = + [[RiveFactory alloc] initWithFactory:[context factory]]; + auto renderImage = [factory decodeImage:data]; + auto image = [renderImage instance]; + if (image == nullptr || image.get() == nullptr) + { + return nil; + } + return [[RiveRenderImage alloc] initWithImage:image]; +} + - (rive::rcp)instance { return instance; diff --git a/Source/Renderer/include/RiveFactory.h b/Source/Renderer/include/RiveFactory.h index 70a0a7c..d3b4b04 100644 --- a/Source/Renderer/include/RiveFactory.h +++ b/Source/Renderer/include/RiveFactory.h @@ -22,6 +22,7 @@ NS_ASSUME_NONNULL_BEGIN @interface RiveRenderImage : NSObject +- (nullable instancetype)initWithData:(NSData*)data; @end @interface RiveAudio : NSObject diff --git a/Source/Renderer/include/RivePrivateHeaders.h b/Source/Renderer/include/RivePrivateHeaders.h index 3880bfd..a2b8cd0 100644 --- a/Source/Renderer/include/RivePrivateHeaders.h +++ b/Source/Renderer/include/RivePrivateHeaders.h @@ -232,4 +232,8 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)initWithData:(rive::PropertyData)data; @end +@interface RiveDataBindingViewModelInstanceImageProperty () +- (instancetype)initWithImage:(rive::ViewModelInstanceAssetImageRuntime*)image; +@end + NS_ASSUME_NONNULL_END diff --git a/Tests/Assets/1x1_jpg.jpg b/Tests/Assets/1x1_jpg.jpg new file mode 100644 index 0000000..844ed5d Binary files /dev/null and b/Tests/Assets/1x1_jpg.jpg differ diff --git a/Tests/Assets/1x1_png.png b/Tests/Assets/1x1_png.png new file mode 100644 index 0000000..621b075 Binary files /dev/null and b/Tests/Assets/1x1_png.png differ diff --git a/Tests/Assets/data_binding_image_test.riv b/Tests/Assets/data_binding_image_test.riv new file mode 100644 index 0000000..e1566e3 Binary files /dev/null and b/Tests/Assets/data_binding_image_test.riv differ diff --git a/Tests/DataBindingTests.swift b/Tests/DataBindingTests.swift index 8a2e437..7f5f771 100644 --- a/Tests/DataBindingTests.swift +++ b/Tests/DataBindingTests.swift @@ -10,7 +10,7 @@ import XCTest @testable import RiveRuntime class DataBindingTests: XCTestCase { - let file = try! RiveFile(testfileName: "data_binding_test") + let file: RiveFile = try! RiveFile(testfileName: "data_binding_test") // MARK: - RiveFile @@ -442,6 +442,28 @@ class DataBindingTests: XCTestCase { XCTAssertNil(instance.triggerProperty(fromPath: "404")) } + // MARK: Image + + func test_viewModelInstance_imageProperty_returnsPropertyOrNil() throws { + let file = try RiveFile(testfileName: "data_binding_image_test") + let instance = file.viewModelNamed("vm")!.createDefaultInstance()! + XCTAssertNotNil(instance.imageProperty(fromPath: "img")) + XCTAssertNil(instance.imageProperty(fromPath: "404")) + } + + func test_viewModelInstance_imageProperty_canSetValue() throws { + let file = try RiveFile(testfileName: "data_binding_image_test") + let instance = file.viewModelNamed("vm")!.createDefaultInstance()! + let property = instance.imageProperty(fromPath: "img")! + + let bundle = Bundle(for: type(of: self)) + let fileURL = bundle.url(forResource: "1x1_jpg", withExtension: "jpg")! + let data = try Data(contentsOf: fileURL) + let renderImage = RiveRenderImage(data: data)! + property.setValue(renderImage) + XCTAssertTrue(property.hasChanged) + } + // MARK: Binding func test_binding_artboard_stringProperty_updatesTextRun() throws { diff --git a/Tests/RiveRenderImageTests.swift b/Tests/RiveRenderImageTests.swift new file mode 100644 index 0000000..1f2d173 --- /dev/null +++ b/Tests/RiveRenderImageTests.swift @@ -0,0 +1,58 @@ +// +// RiveRenderImageTests.swift +// RiveRuntimeTests +// +// Created by David Skuza on 5/13/25. +// Copyright © 2025 Rive. All rights reserved. +// + +import XCTest +import RiveRuntime + +class RiveRenderImageTests: XCTestCase { + func test_imageFromData_withEmptyData_returnsNil() { + // Data comprised of 0 bytes + XCTAssertNil(RiveRenderImage(data: Data())) + } + + func test_imageFromData_withIncompatibleData_returnsNil() throws { + // Data comprised of 8 bytes that do _not_ create an image + XCTAssertNil(RiveRenderImage(data: Data([1, 2, 3, 4, 5, 6, 7, 8]))) + } + + func test_imageFromData_withCompatibleData_returnsImage() throws { + let bundle = Bundle(for: type(of: self)) + + // We know JPG to be a valid Rive image asset format + var fileURL = bundle.url(forResource: "1x1_jpg", withExtension: "jpg")! + var data = try Data(contentsOf: fileURL) + XCTAssertNotNil(RiveRenderImage(data: data)) + + // We know PNG to be a valid Rive image asset format + fileURL = bundle.url(forResource: "1x1_png", withExtension: "png")! + data = try Data(contentsOf: fileURL) + XCTAssertNotNil(RiveRenderImage(data: data)) + } + + // MARK: - Extensions + + func test_imageFromUIImage_withIncorrectFormat_returnsNil() throws { + XCTAssertNil(RiveRenderImage(image: UIImage(), format: .png)) + XCTAssertNil(RiveRenderImage(image: UIImage(), format: .jpeg(compressionQuality: 80))) + } + + func test_imageFromUIImage_withCorrectFormat_returnsImage() throws { + let bundle = Bundle(for: type(of: self)) + + // We know JPG to be a valid Rive image asset format + var fileURL = bundle.url(forResource: "1x1_jpg", withExtension: "jpg")! + var data = try Data(contentsOf: fileURL) + var image = UIImage(data: data)! + XCTAssertNotNil(RiveRenderImage(image: image, format: .jpeg(compressionQuality: 80))) + + fileURL = bundle.url(forResource: "1x1_png", withExtension: "png")! + data = try Data(contentsOf: fileURL) + image = UIImage(data: data)! + XCTAssertNotNil(RiveRenderImage(image: image, format: .png)) + } +}