From 618b1eb8bf88f13d678ca3b4dcc76fdb0f65ec03 Mon Sep 17 00:00:00 2001 From: Maxwell Talbot Date: Thu, 27 May 2021 13:37:42 +0100 Subject: [PATCH 1/3] simplify loading bytes --- Example-iOS/Source/lib/utility.swift | 24 +++++------ Source/Renderer/RiveFile.mm | 61 +++++++++++++++++++++------- Source/Renderer/include/RiveFile.h | 1 + 3 files changed, 57 insertions(+), 29 deletions(-) diff --git a/Example-iOS/Source/lib/utility.swift b/Example-iOS/Source/lib/utility.swift index 5ac6d1d3..489e1c05 100644 --- a/Example-iOS/Source/lib/utility.swift +++ b/Example-iOS/Source/lib/utility.swift @@ -9,26 +9,22 @@ import Foundation import RiveRuntime -func getRiveFile(resourceName: String, resourceExt: String=".riv") -> RiveFile { +func getBytes(resourceName: String, resourceExt: String=".riv") -> [UInt8] { guard let url = Bundle.main.url(forResource: resourceName, withExtension: resourceExt) else { fatalError("Failed to locate \(resourceName) in bundle.") } - guard var data = try? Data(contentsOf: url) else { + guard let data = try? Data(contentsOf: url) else { fatalError("Failed to load \(url) from bundle.") } // Import the data into a RiveFile - let bytes = [UInt8](data) - - return data.withUnsafeMutableBytes{(riveBytes:UnsafeMutableRawBufferPointer)->RiveFile in - guard let rawPointer = riveBytes.baseAddress else { - fatalError("File pointer is messed up") - } - let pointer = rawPointer.bindMemory(to: UInt8.self, capacity: bytes.count) - - guard let riveFile = RiveFile(bytes:pointer, byteLength: UInt64(bytes.count)) else { - fatalError("Failed to import \(url).") - } - return riveFile + return [UInt8](data) +} + +func getRiveFile(resourceName: String, resourceExt: String=".riv") -> RiveFile{ + let byteArray = getBytes(resourceName: resourceName, resourceExt: resourceExt) + guard let riveFile = RiveFile(byteArray: byteArray) else { + fatalError("Failed to import Rive File.") } + return riveFile } diff --git a/Source/Renderer/RiveFile.mm b/Source/Renderer/RiveFile.mm index 338ebd3f..402c12a7 100644 --- a/Source/Renderer/RiveFile.mm +++ b/Source/Renderer/RiveFile.mm @@ -10,6 +10,13 @@ #import #import +@interface RiveFile () + +- (rive::BinaryReader) getReader:(UInt8 *)bytes byteLength:(UInt64)length; +- (void) import:(rive::BinaryReader)reader; + +@end + /* * RiveFile */ @@ -17,26 +24,50 @@ @implementation RiveFile { rive::File* riveFile; } +- (rive::BinaryReader) getReader:(UInt8 *)bytes byteLength:(UInt64)length { + return rive::BinaryReader(bytes, length); +} + + + (uint)majorVersion { return UInt8(rive::File::majorVersion); } + (uint)minorVersion { return UInt8(rive::File::minorVersion); } +- (void) import:(rive::BinaryReader)reader { + rive::ImportResult result = rive::File::import(reader, &riveFile); + if (result == rive::ImportResult::success) { + return; + } + else if(result == rive::ImportResult::unsupportedVersion){ + @throw [[RiveException alloc] initWithName:@"UnsupportedVersion" reason:@"Unsupported Rive File Version." userInfo:nil]; + + } + else if(result == rive::ImportResult::malformed){ + @throw [[RiveException alloc] initWithName:@"Malformed" reason:@"Malformed Rive File." userInfo:nil]; + } + else { + @throw [[RiveException alloc] initWithName:@"Unknown" reason:@"Unknown error loading file." userInfo:nil]; + } +} + +- (nullable instancetype)initWithByteArray:(NSMutableArray *)array { + if (self = [super init]) { + UInt8* bytes = (UInt8*)calloc(array.count, sizeof(UInt64)); + [array enumerateObjectsUsingBlock:^(NSNumber* number, NSUInteger index, BOOL* stop){ + bytes[index] = number.unsignedIntValue; + }]; + rive::BinaryReader reader = [self getReader:bytes byteLength:array.count]; + free(bytes); + [self import:reader]; + return self; + } + return nil; +} + - (nullable instancetype)initWithBytes:(UInt8 *)bytes byteLength:(UInt64)length { if (self = [super init]) { - rive::BinaryReader reader = rive::BinaryReader(bytes, length); - rive::ImportResult result = rive::File::import(reader, &riveFile); - if (result == rive::ImportResult::success) { - return self; - } - else if(result == rive::ImportResult::unsupportedVersion){ - @throw [[RiveException alloc] initWithName:@"UnsupportedVersion" reason:@"Unsupported Rive File Version." userInfo:nil]; - - } - else if(result == rive::ImportResult::malformed){ - @throw [[RiveException alloc] initWithName:@"Malformed" reason:@"Malformed Rive File." userInfo:nil]; - } - else { - @throw [[RiveException alloc] initWithName:@"Unknown" reason:@"Unknown error loading file." userInfo:nil]; - } + rive::BinaryReader reader = [self getReader:bytes byteLength:length]; + [self import:reader]; + return self; } return nil; } diff --git a/Source/Renderer/include/RiveFile.h b/Source/Renderer/include/RiveFile.h index 6e77f612..342e9323 100644 --- a/Source/Renderer/include/RiveFile.h +++ b/Source/Renderer/include/RiveFile.h @@ -24,6 +24,7 @@ NS_ASSUME_NONNULL_BEGIN @property (class, readonly) uint majorVersion; @property (class, readonly) uint minorVersion; +- (nullable instancetype)initWithByteArray:(NSArray *)bytes; - (nullable instancetype)initWithBytes:(UInt8 *)bytes byteLength:(UInt64)length; // Returns a reference to the default artboard From 64f56f5b6a45d1b2182b10684752aa894bbdf4db Mon Sep 17 00:00:00 2001 From: Maxwell Talbot Date: Thu, 27 May 2021 14:24:12 +0100 Subject: [PATCH 2/3] adopt simpler file reading in tests --- Source/Renderer/RiveFile.mm | 23 +++++++++++++++-------- Tests/RiveDelegatesTest.swift | 26 ++++++++++---------------- 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/Source/Renderer/RiveFile.mm b/Source/Renderer/RiveFile.mm index 402c12a7..2ea61c1c 100644 --- a/Source/Renderer/RiveFile.mm +++ b/Source/Renderer/RiveFile.mm @@ -49,15 +49,22 @@ - (void) import:(rive::BinaryReader)reader { } } -- (nullable instancetype)initWithByteArray:(NSMutableArray *)array { +- (nullable instancetype)initWithByteArray:(NSArray *)array { if (self = [super init]) { - UInt8* bytes = (UInt8*)calloc(array.count, sizeof(UInt64)); - [array enumerateObjectsUsingBlock:^(NSNumber* number, NSUInteger index, BOOL* stop){ - bytes[index] = number.unsignedIntValue; - }]; - rive::BinaryReader reader = [self getReader:bytes byteLength:array.count]; - free(bytes); - [self import:reader]; + UInt8* bytes; + @try { + bytes = (UInt8*)calloc(array.count, sizeof(UInt64)); + + [array enumerateObjectsUsingBlock:^(NSNumber* number, NSUInteger index, BOOL* stop){ + bytes[index] = number.unsignedIntValue; + }]; + rive::BinaryReader reader = [self getReader:bytes byteLength:array.count]; + [self import:reader]; + } + @finally { + free(bytes); + } + return self; } return nil; diff --git a/Tests/RiveDelegatesTest.swift b/Tests/RiveDelegatesTest.swift index 3554438a..a6372f19 100644 --- a/Tests/RiveDelegatesTest.swift +++ b/Tests/RiveDelegatesTest.swift @@ -9,30 +9,24 @@ import XCTest import RiveRuntime -func getRiveFile(resourceName: String, resourceExt: String=".riv") -> RiveFile { - -// bundle.resourceURL +func getBytes(resourceName: String, resourceExt: String=".riv") -> [UInt8] { guard let url = Bundle(for: DelegatesTest.self).url(forResource: resourceName, withExtension: resourceExt) else { fatalError("Failed to locate \(resourceName) in bundle.") } - guard var data = try? Data(contentsOf: url) else { + guard let data = try? Data(contentsOf: url) else { fatalError("Failed to load \(url) from bundle.") } // Import the data into a RiveFile - let bytes = [UInt8](data) - - return data.withUnsafeMutableBytes{(riveBytes:UnsafeMutableRawBufferPointer)->RiveFile in - guard let rawPointer = riveBytes.baseAddress else { - fatalError("File pointer is messed up") - } - let pointer = rawPointer.bindMemory(to: UInt8.self, capacity: bytes.count) - - guard let riveFile = RiveFile(bytes:pointer, byteLength: UInt64(bytes.count)) else { - fatalError("Failed to import \(url).") - } - return riveFile + return [UInt8](data) +} + +func getRiveFile(resourceName: String, resourceExt: String=".riv") -> RiveFile{ + let byteArray = getBytes(resourceName: resourceName, resourceExt: resourceExt) + guard let riveFile = RiveFile(byteArray: byteArray) else { + fatalError("Failed to import Rive File.") } + return riveFile } class MrDelegate: LoopDelegate, PlayDelegate, PauseDelegate, StopDelegate, StateChangeDelegate { From 9a0884de6f4201a78d57008738aaf8916202fd11 Mon Sep 17 00:00:00 2001 From: Maxwell Talbot Date: Thu, 27 May 2021 15:54:27 +0100 Subject: [PATCH 3/3] spruce up readme a bit more --- .../Source/UIkit/SimpleAnimation.swift | 21 +- README.md | 187 +++++++++++++++++- 2 files changed, 197 insertions(+), 11 deletions(-) diff --git a/Example-iOS/Source/UIkit/SimpleAnimation.swift b/Example-iOS/Source/UIkit/SimpleAnimation.swift index 379c6b08..a9ffdf92 100644 --- a/Example-iOS/Source/UIkit/SimpleAnimation.swift +++ b/Example-iOS/Source/UIkit/SimpleAnimation.swift @@ -9,16 +9,31 @@ import UIKit import RiveRuntime +func getResourceBytes(resourceName: String, resourceExt: String=".riv") -> [UInt8] { + guard let url = Bundle.main.url(forResource: resourceName, withExtension: resourceExt) else { + fatalError("Failed to locate \(resourceName) in bundle.") + } + guard let data = try? Data(contentsOf: url) else { + fatalError("Failed to load \(url) from bundle.") + } + + // Import the data into a RiveFile + return [UInt8](data) +} + class SimpleAnimationViewController: UIViewController { let resourceName = "truck_v7" - override public func loadView() { super.loadView() - // Wire up an instance of RiveView to the controller + let view = RiveView() + guard let riveFile = RiveFile(byteArray: getResourceBytes(resourceName: resourceName)) else { + fatalError("Failed to load RiveFile") + } + view.configure(riveFile) + self.view = view - (self.view! as! RiveView).configure(getRiveFile(resourceName: resourceName)) } } diff --git a/README.md b/README.md index 6d1b8050..bbd613ba 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,6 @@ iOS runtime for [Rive](https://rive.app/) This is the Android runtime for [Rive](https://rive.app), currently in beta. The api is subject to change as we continue to improve it. Please file issues and PRs for anything busted, missing, or just wrong. - # Installing rive-ios ## Via github @@ -19,13 +18,13 @@ You can clone this repository and include the RiveRuntime.xcodeproj to build a d When pulling down this repo, you'll need to make sure to also pull down the submodule which contains the C++ runtime that our iOS runtime is built upon. The easiest way to do this is to run this: -``` +```sh git clone --recurse-submodules git@github.com:rive-app/rive-ios ``` When updating, remember to also update the submodule with the same command. -``` +```sh git submodule update --init ``` @@ -35,16 +34,188 @@ We are in the process of getting a pod available in [cocoapods](https://cocoapod While we are working out any kinks, we are publishing our pod to a temporary github repo, which you can install by including placeholder while we finalize any kinks. -``` +```ruby pod 'RiveRuntime', :git => 'git@github.com:rive-app/test-ios.git' ``` -Once you have installed the pod, you can run `import RiveRuntime` to have access to our higher level views or build on top of our bindings to control your own animation loop. +Once you have installed the pod, you can run + +```swift +import RiveRuntime +``` + +to have access to our higher level views or build on top of our bindings to control your own animation loop. # Examples -There is an example project next to the runtimes. +There is an example project next to the runtimes. + +The examples show simple ways of adding animated views into your app, how to add buttons & slider controls, how to use state machines & how to navigate the contents of a rive file programatically. + +To run the example, open the `Rive.xcworkspace` in Xcode and run the `RiveExample` project. + +# Overview + +We have provided high level Swift controller and a UIkit view to easily add rive into your application. All of this is built ontop of an objective c layer that allows for fine grained granular animation control. + +## UIKit + +### RiveView + +The simplest way of adding a riveView to a controller is probably to just set it as the controllers view when it is loaded. + +```swift +class SimpleAnimationViewController: UIViewController { + let resourceName = "truck_v7" + + override public func loadView() { + super.loadView() + + guard let riveFile = RiveFile(byteArray: getResourceBytes(resourceName: resourceName)) else { + fatalError("Failed to load RiveFile") + } + let view = RiveView(riveFile:riveFile) + self.view = view + } +} +``` + +The RiveView will autoplay the first animation found in the riveFile, our code for loading the local resource as UInt8 data is here. + +```swift +func getResourceBytes(resourceName: String, resourceExt: String=".riv") -> [UInt8] { + guard let url = Bundle.main.url(forResource: resourceName, withExtension: resourceExt) else { + fatalError("Failed to locate \(resourceName) in bundle.") + } + guard let data = try? Data(contentsOf: url) else { + fatalError("Failed to load \(url) from bundle.") + } + + // Import the data into a RiveFile + return [UInt8](data) +} +``` + +The riveView can be further customized to select which animation to play, or how to fit the animation into the view space. A lot of configuration is possible on the RiveView, playback controls are added as functions on the view, and to change which file or artboard is being displayed, use `.configure`. + +### Layout + +The rive view can be further customized as part of specifying layout attributes. + +fit can be specified to determine how the animation should be resized to fit its container. The available choices are `.fitFill` , `.fitContain` , `.fitCover` , `.fitFitWidth` , `.fitFitHeight` , `.fitNone` , `.fitScaleDown` + +alignment informs how it should be aligned within the container. The available choices are `alignmentTopLeft`, `alignmentTopCenter`, `alignmentTopRight`, `alignmentCenterLeft`, `alignmentCenter`, `alignmentCenterRight`, `alignmentBottomLeft`, `alignmentBottomCenter`, `alignmentBottomRight`. + +This can be specified when instantiating the view + +```swift +let view = RiveView( + riveFile:riveFile, + fit: .fitFill, + alignment: .alignmentBottomLeft +) +``` + +or anytime afterwards. + +```swift +view.fit = .fitCover +view.alignment = .alignmentCenter +``` + +### Playback Controls -The examples show simple ways of adding animated views into your app, how to add buttons & slider controls, how to use state machines & how to navigate the contents of a rive file programatically. +Animations can be controlled in many ways, by default loading a RiveView with a riveFile will autoplay the first animation on the first artboard. The artboard and animation can be specified by name here. -To run the example, open the `Rive.xcworkspace` in Xcode and run the `RiveExample` project. \ No newline at end of file +```swift +let riveView = RiveView( + riveFile: riveFile, + fit: .fitContain, + alignment: .alignmentCenter, + artboard: "Square", + animation: "rollaround", + autoplay: true +) +``` + +furthermore animations can be controlled later too: + +To play an animation named rollaround. + +```swift +riveView.play(animationName: "rollaround") +``` + +multiple animations can play at the same time, and additional animations can be added at any time + +```swift +riveView.play( + animationNames: ["bouncing", "windshield_wipers"] +) +``` + +When playing animations, the Loop Mode and direction of the animations can also be set per animation. + +```swift +riveView.play( + animationNames: ["bouncing", "windshield_wipers"], + loop: .loopOneShot, + direction: .directionBackwards +) +``` + +Similarly animations can be paused, or stopped, either all at the same time, or one by one. + +```swift +riveView.stop() +riveView.stop(animationName:"bouncing") +riveView.stop(animationNames:["bouncing", "windshield_wipers"]) +``` + +```swift +riveView.pause() +riveView.pause(animationName:"bouncing") +riveView.pause(animationNames:["bouncing", "windshield_wipers"]) +``` + +### Mixing + +Mixing goes further than just playing multiple animations at the same time, animations can use a mix factor between 0 and 1, to allow multiple animations effects to blend together. The high level views do not expose this currently. but you can wrap your own render loop around the core libraries. The advance function is where you can specify a mix factor. + +### Delegates & Events + +The rive ios runtimes allow for delegates that can be provided to the RiveView. If provided these delegates will be fired whenever a matching event is triggered. + +There are the following delegates `LoopDelegate`, `PlayDelegate`, `PauseDelegate`, `StopDelegate`, `StateChangeDelegate` + +You can crete your own delegate like this, implementing as many protocols are are needed. + +```swift +class MyDelegate: PlayDelegate, LoopDelegate { + func loop(_ animationName: String, type: Int) { + // do things when the animation loops playing. + } + + func play(_ animationName: String, isStateMachine: Bool) { + // do things when the animation starts playing. + } +} +``` + +To use a delegate simply pass it to the view on instantiation + +```swift +let delegate = MyDelegate() + +let view = RiveView( + riveFile:riveFile, + loopDelegate: delegate, + playDelegate: delegate, +) +``` + +or attach it later + +```swift +view.loopDelegate = delegate +```