Skip to content

Commit

Permalink
Merge pull request #8 from rive-app/example_doc_updates
Browse files Browse the repository at this point in the history
Updates example and expands README
  • Loading branch information
mjohnsullivan authored Nov 18, 2020
2 parents 16b6af1 + 22e62ca commit 565e224
Show file tree
Hide file tree
Showing 6 changed files with 282 additions and 25 deletions.
Binary file removed Example-iOS/Assets/blend.riv
Binary file not shown.
Binary file modified Example-iOS/Assets/funtime.riv
Binary file not shown.
Binary file removed Example-iOS/Assets/simple.riv
Binary file not shown.
8 changes: 0 additions & 8 deletions Example-iOS/RiveExample.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@

/* Begin PBXBuildFile section */
C9696B1224FC704A0041502A /* juice.riv in Resources */ = {isa = PBXBuildFile; fileRef = C9696B1124FC704A0041502A /* juice.riv */; };
C970484A250822F300CB3AB3 /* simple.riv in Resources */ = {isa = PBXBuildFile; fileRef = C9704847250822F300CB3AB3 /* simple.riv */; };
C98F298B2513FD8C0076E802 /* blend.riv in Resources */ = {isa = PBXBuildFile; fileRef = C98F29882513FD8C0076E802 /* blend.riv */; };
C9927C1B250986BF009F17F5 /* truck.riv in Resources */ = {isa = PBXBuildFile; fileRef = C9927C18250986BF009F17F5 /* truck.riv */; };
C99E2FE3251E8DB4009227CA /* van.riv in Resources */ = {isa = PBXBuildFile; fileRef = C99E2FE0251E8DB4009227CA /* van.riv */; };
C99E2FE8251E8E35009227CA /* bird.riv in Resources */ = {isa = PBXBuildFile; fileRef = C99E2FE6251E8E35009227CA /* bird.riv */; };
Expand Down Expand Up @@ -61,8 +59,6 @@

/* Begin PBXFileReference section */
C9696B1124FC704A0041502A /* juice.riv */ = {isa = PBXFileReference; lastKnownFileType = file; path = juice.riv; sourceTree = "<group>"; };
C9704847250822F300CB3AB3 /* simple.riv */ = {isa = PBXFileReference; lastKnownFileType = file; path = simple.riv; sourceTree = "<group>"; };
C98F29882513FD8C0076E802 /* blend.riv */ = {isa = PBXFileReference; lastKnownFileType = file; path = blend.riv; sourceTree = "<group>"; };
C9927C18250986BF009F17F5 /* truck.riv */ = {isa = PBXFileReference; lastKnownFileType = file; path = truck.riv; sourceTree = "<group>"; };
C99E2FE0251E8DB4009227CA /* van.riv */ = {isa = PBXFileReference; lastKnownFileType = file; path = van.riv; sourceTree = "<group>"; };
C99E2FE6251E8E35009227CA /* bird.riv */ = {isa = PBXFileReference; lastKnownFileType = file; path = bird.riv; sourceTree = "<group>"; };
Expand Down Expand Up @@ -102,9 +98,7 @@
C99E2FE6251E8E35009227CA /* bird.riv */,
C99E2FE7251E8E35009227CA /* funtime.riv */,
C99E2FE0251E8DB4009227CA /* van.riv */,
C98F29882513FD8C0076E802 /* blend.riv */,
C9927C18250986BF009F17F5 /* truck.riv */,
C9704847250822F300CB3AB3 /* simple.riv */,
C9696B1124FC704A0041502A /* juice.riv */,
);
path = Assets;
Expand Down Expand Up @@ -252,14 +246,12 @@
buildActionMask = 2147483647;
files = (
C99E2FE9251E8E35009227CA /* funtime.riv in Resources */,
C98F298B2513FD8C0076E802 /* blend.riv in Resources */,
C9696B1224FC704A0041502A /* juice.riv in Resources */,
C9C73EA424FC471E00EF9516 /* LaunchScreen.storyboard in Resources */,
C99E2FE3251E8DB4009227CA /* van.riv in Resources */,
C9EEA13125268E48001BB768 /* teeny.riv in Resources */,
C99E2FE8251E8E35009227CA /* bird.riv in Resources */,
C9C73EA124FC471E00EF9516 /* Preview Assets.xcassets in Resources */,
C970484A250822F300CB3AB3 /* simple.riv in Resources */,
C9C73E9E24FC471E00EF9516 /* Assets.xcassets in Resources */,
C9927C1B250986BF009F17F5 /* truck.riv in Resources */,
C9A7D7F52523C1D900AFB875 /* sheep.riv in Resources */,
Expand Down
26 changes: 10 additions & 16 deletions Example-iOS/Source/MyRiveViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ class MyRiveViewController: UIViewController {
var resourceName: String?
var resourceExt: String?
var artboard: RiveArtboard?
var instance0: RiveLinearAnimationInstance?
var instance1: RiveLinearAnimationInstance?
var instance: RiveLinearAnimationInstance?
var displayLink: CADisplayLink?
var lastTime: CFTimeInterval = 0

Expand Down Expand Up @@ -76,17 +75,15 @@ class MyRiveViewController: UIViewController {
fatalError("No animations in the file.")
}

// Fetch two animations
let animation0 = artboard.animation(at: 0)
self.instance0 = animation0.instance()
// Fetch an animation
let animation = artboard.animation(at: 0)
self.instance = animation.instance()

let animation1 = artboard.animation(at: 1)
self.instance1 = animation1.instance()

// Advance the artboard
// Advance the artboard, this will ensure the first
// frame is displayed when the artboard is drawn
artboard.advance(by: 0)

// Run the looping timer
// Start the animation loop
runTimer()
}
}
Expand Down Expand Up @@ -120,12 +117,9 @@ class MyRiveViewController: UIViewController {
lastTime = timestamp;

// Advance the animation instance and the artboard
instance0!.advance(by: elapsedTime) // advance the animation
instance0!.apply(to: artboard) // apply to the artboard

instance1!.advance(by: elapsedTime)
instance1!.apply(to: artboard)

instance!.advance(by: elapsedTime) // advance the animation
instance!.apply(to: artboard) // apply to the artboard

artboard.advance(by: elapsedTime) // advance the artboard

// Trigger a redraw
Expand Down
273 changes: 272 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,275 @@
# rive-ios
iOS runtime for Rive

To run the example, open the project in ```Example-iOS``` and run that. The playing animation can be changed by editing ```RiveViewController(resource: "xxx", withExtension: "riv")``` in ```ContentView.swift```.
## Running the Example
To run the example, open the project in [Example-iOS directory](https://github.com/rive-app/rive-ios/tree/master/Example-iOS). The playing animation can be changed by editing ```RiveViewController(resource: "xxx", withExtension: "riv")``` in ```ContentView.swift```.

## Runtime Granularity
This is a low-level runtime, designed to give you complete control over Rive animations and how they're drawn. As such, it requires you to do some heavy-lifting and control the animation loop and timing.

We took this decision as we felt that it was important to give you as total control of your animations, and to provide a solid basis for higher-level runtimes that can implement pre-baked controllers and views for embedding Rive animations.

The runtime has two dependencies: CoreGraphics and QuartzCore. We intentionally omitted binding to UIKit or AppKit to keep the runtime generic, the intent being for higher-level runtimes to provide support for iOS and MacOS.

## Runtime Implementation
The runtime is implemented in Objective-C. Why not Swift? Rive's runtimes for the web, Android, and iOS all share a common C++ core, which implements Rive's file loading, animation control, and rendering. Objective-C++ interoperates well with C++, hence our choice. Our example code is implemented in Swift.

## Playing a Looping Animation

Let's go through a simple example of how to embed a looping animation inside your app. We're going to create a custom UIViewController and UIView that you can drop into your UI.

First thing we need to do in our controller is load a Rive file. For this, we can use the ```RiveFile``` class, which takes a byte list and length. The bytes can be loaded in whatever manner you choose: from a file or over a network; here we're loading it from the bundle.

```swift
func startRive() {
guard let name = resourceName, let ext = resourceExt else {
fatalError("No resource or extension name specified")
}
guard let url = Bundle.main.url(forResource: name, withExtension: ext) else {
fatalError("Failed to locate \(name) in bundle.")
}
guard var data = try? Data(contentsOf: url) else {
fatalError("Failed to load \(url) from bundle.")
}

// Import the data into a RiveFile
let bytes = [UInt8](data)

data.withUnsafeMutableBytes{(riveBytes:UnsafeMutableRawBufferPointer) 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).")
}

// More to come ...
}
}
```

Most of this code loads the raw byte data; the ```RiveFile(bytes:pointer, byteLength: UInt64(bytes.count))``` constructor initializes the animation data.

Next, we'll need to get a reference to the artboard that contains the animation we want to play. Once we have that, we access one of the animations in the artboard and create an instance from it. Then we start the animation loop.

```swift
let artboard = riveFile.artboard()

if (artboard.animationCount() == 0) {
fatalError("No animations in the file.")
}

// Fetch an animation
let animation = artboard.animation(at: 0)

// Advance the artboard, this will ensure the first
// frame is displayed when the artboard is drawn
artboard.advance(by: 0)

// Start the animation loop
runTimer()
```

As this is a low-level runtime, you have to create and manage your own animation loop. Fortunately, Apple's made this easy with ```CADisplayLink```. Here's the code to run the loop and play the animation instance.

```swift
// Starts the animation timer
func runTimer() {
displayLink = CADisplayLink(target: self, selector: #selector(tick));
displayLink?.add(to: .main, forMode: .default)
}

// Stops the animation timer
func stopTimer() {
displayLink?.remove(from: .main, forMode: .default)
}

// Animates a frame
@objc func tick() {
guard let displayLink = displayLink, let artboard = artboard else {
// Something's gone wrong, clean up and bug out
stopTimer()
return
}

let timestamp = displayLink.timestamp
// last time needs to be set on the first tick
if (lastTime == 0) {
lastTime = timestamp
}
// Calculate the time elapsed between ticks
let elapsedTime = timestamp - lastTime;
lastTime = timestamp;

// Advance the animation instance and the artboard
instance!.advance(by: elapsedTime) // advance the animation
instance!.apply(to: artboard) // apply to the artboard

artboard.advance(by: elapsedTime) // advance the artboard

// Trigger a redraw
self.view.setNeedsDisplay()
}
```

```tick``` is where the Rive magic happens. We first calculate how much time has elapsed since the last call to tick, storing it in ```elapsedTime```. We then advance our animation by the elapsed time, and apply the animation to the artboard. We then advance the artboard by the elapsed time. Finally, we tell our view to redraw.

Why advance the both animation instance and artboard, and not do it in one step? Rive lets you mix animations together, so you can easily apply multiple animations to the same artboard to create sophisticated animation behavior. You could also apply these animations at different elapsed times, giving you complete control over the speed of how these animations mix.

Here's the complete code for our controller:

```swift
import UIKit
import RiveRuntime

class MyRiveViewController: UIViewController {

var resourceName: String?
var resourceExt: String?
var artboard: RiveArtboard?
var instance: RiveLinearAnimationInstance?
var displayLink: CADisplayLink?
var lastTime: CFTimeInterval = 0

init(withResource name: String, withExtension: String) {
resourceName = name
resourceExt = withExtension
super.init(nibName: nil, bundle: nil)
}

required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}

override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
startRive()
}

override func loadView() {
// Wire up an instance of MyRiveView to the controller
let view = MyRiveView()
view.backgroundColor = UIColor.blue
self.view = view
}

func startRive() {
guard let name = resourceName, let ext = resourceExt else {
fatalError("No resource or extension name specified")
}
guard let url = Bundle.main.url(forResource: name, withExtension: ext) else {
fatalError("Failed to locate \(name) in bundle.")
}
guard var data = try? Data(contentsOf: url) else {
fatalError("Failed to load \(url) from bundle.")
}

// Import the data into a RiveFile
let bytes = [UInt8](data)

data.withUnsafeMutableBytes{(riveBytes:UnsafeMutableRawBufferPointer) 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).")
}

let artboard = riveFile.artboard()

self.artboard = artboard
// update the artboard in the view
(self.view as! MyRiveView).updateArtboard(artboard)

if (artboard.animationCount() == 0) {
fatalError("No animations in the file.")
}

// Fetch an animation
let animation = artboard.animation(at: 0)
self.instance = animation.instance()

// Advance the artboard, this will ensure the first
// frame is displayed when the artboard is drawn
artboard.advance(by: 0)

// Start the animation loop
runTimer()
}
}

// Starts the animation timer
func runTimer() {
displayLink = CADisplayLink(target: self, selector: #selector(tick));
displayLink?.add(to: .main, forMode: .default)
}

// Stops the animation timer
func stopTimer() {
displayLink?.remove(from: .main, forMode: .default)
}

// Animates a frame
@objc func tick() {
guard let displayLink = displayLink, let artboard = artboard else {
// Something's gone wrong, clean up and bug out
stopTimer()
return
}

let timestamp = displayLink.timestamp
// last time needs to be set on the first tick
if (lastTime == 0) {
lastTime = timestamp
}
// Calculate the time elapsed between ticks
let elapsedTime = timestamp - lastTime;
lastTime = timestamp;

// Advance the animation instance and the artboard
instance!.advance(by: elapsedTime) // advance the animation
instance!.apply(to: artboard) // apply to the artboard

artboard.advance(by: elapsedTime) // advance the artboard

// Trigger a redraw
self.view.setNeedsDisplay()
}
}
```

We still need to draw our rendered animation through the view. The code's pretty short; here's the view in its entirety:

```swift
import UIKit
import RiveRuntime

class MyRiveView: UIView {

var artboard: RiveArtboard?;

func updateArtboard(_ artboard: RiveArtboard) {
self.artboard = artboard;
}

override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext(), let artboard = self.artboard else {
return
}
let renderer = RiveRenderer(context: context);
renderer.align(with: rect, withContentRect: artboard.bounds(), with: Alignment.Center, with: Fit.Contain)
artboard.draw(renderer)
}
}
```

Over view is overriding its ```draw``` function and on every frame is creating a RiveRenderer, which renders an artboard to the UI. Our controller sets the artboard during initializtion and drives the view to update on every ```tick``` with ```self.view.setNeedsDisplay()```.

You can place this view in an app, and the runtime will continuously play the looping animation. If it's a one-shot animation, it'll play it once and then stop.

The code for this example is in the [Example-iOS directory](https://github.com/rive-app/rive-ios/tree/master/Example-iOS).

0 comments on commit 565e224

Please sign in to comment.