Add fallback font support for iOS and macOS

This builds on top of #7661 and adds an iOS / macOS API for setting fallback fonts.

At a high-level, this adds a class property to `RiveFont` for getting / setting the fallback font(s) **based on the system** (or _optionally_, a `UIFont/NSFont`)(accessible in Objective-C and Swift via `RiveFont.fallbackFonts`). This property is an array of a objects conforming to a new protocol: `RiveFallbackFontProvider` (platform-agnostic). By default, if no fallbacks are set, or an empty array is set, the default system font of regular weight will be used.

In terms of naming, the `RiveFallbackFontDescriptorDesign` and `RiveFallbackFontDescriptorWeight` types each have cases that mirror those available in 1st party Apple APIs, so that usage and expectations are similar across our APIs, as well as those provided by UIKit / AppKit.

## Example Usage

```swift
RiveFont.fallbackFonts = [
  RiveFallbackFontDescriptor(systemDesign: .default, weight: .bold),
  RiveFallbackFontDescriptor(systemDesign: .monospaced, weight: .ultraLight),
  UIFont.systemFont(ofSize: 20, weight: .bold)
]
```

## RiveFallbackFontProvider

`RiveFallbackFontProvider` is a protocol that defines the interface for types that can be used to return system fonts, or any font that can be used as a fallback. `RiveFallbackFontDescriptor` and `UIFont/NSFont` conform to this protocol; both can be used to define fallback fonts.

## RiveFallbackFontDescriptor

`RiveFallbackFontDescriptor` is a platform-agnostic way of defining the _type_ of system font you want to request as a fallback, if necessary. It contains a couple of properties: `design` and `weight`. These are used in conjunction with each other to start with and update a system font (generated by `[UI/NS]Font.systemFont(ofSize:weight:)`, potentially matching on more than one font.

## Unit Tests

Unit tests have been written to verify that `design` and `weight` create different fonts, based on the provided values. The tests at a high-level are the same: for each case of both properties, check that there is at least one matching font. For each property, check that each font name is unique. On iOS, the font names are unique based on system design _and_ weight. I felt this was better than asserting against a specific font name, in case Apple changes that from under our feet. Additionally, what the default system fallback is set to is also tested.

## IRL Testing

This was tested by creating a riv file that contained a text run whose font was exported containing only the glyphs used, and setting the text run to some text that did not use the exported glyphs.

Tested on:
- [x] iOS (Simulator)
- [x] iPadOS
- [x] macOS

Diffs=
a4e15fb7b Add fallback font support for iOS and macOS (#7690)

Co-authored-by: David Skuza <david@rive.app>
This commit is contained in:
dskuza
2024-09-10 13:25:49 +00:00
parent 135d98d4da
commit b0474633b8
13 changed files with 654 additions and 4 deletions

Binary file not shown.

View File

@@ -316,6 +316,10 @@
E5B5C2192B238829006E57C8 /* asset_load_check.riv in Resources */ = {isa = PBXBuildFile; fileRef = E5B5C2172B238829006E57C8 /* asset_load_check.riv */; };
E5CD7D7127DC331900BFE5E2 /* SwiftMeshAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5CD7D7027DC331900BFE5E2 /* SwiftMeshAnimation.swift */; };
E5E87A012AE5A83800E7295F /* SwiftVariableFPS.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5E87A002AE5A83700E7295F /* SwiftVariableFPS.swift */; };
F2C623362C874E3A0006E0CA /* fallback_fonts.riv in Resources */ = {isa = PBXBuildFile; fileRef = F2C623352C874E3A0006E0CA /* fallback_fonts.riv */; };
F2C623372C874E3A0006E0CA /* fallback_fonts.riv in Resources */ = {isa = PBXBuildFile; fileRef = F2C623352C874E3A0006E0CA /* fallback_fonts.riv */; };
F2C623392C874E690006E0CA /* SwiftFallbackFonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2C623382C874E690006E0CA /* SwiftFallbackFonts.swift */; };
F2C6233A2C874E690006E0CA /* SwiftFallbackFonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2C623382C874E690006E0CA /* SwiftFallbackFonts.swift */; };
F8772AF02AD94A4400AB5920 /* marty.riv in Resources */ = {isa = PBXBuildFile; fileRef = 83C89ACE2988709400044C17 /* marty.riv */; };
F8772AF12AD94A4400AB5920 /* paper.riv in Resources */ = {isa = PBXBuildFile; fileRef = 83C89AD0298870A700044C17 /* paper.riv */; };
F8772AF22AD94A4400AB5920 /* Bear.riv in Resources */ = {isa = PBXBuildFile; fileRef = 83DE4CB42AB3974300B88B72 /* Bear.riv */; };
@@ -472,6 +476,8 @@
E5B5C2172B238829006E57C8 /* asset_load_check.riv */ = {isa = PBXFileReference; lastKnownFileType = file; path = asset_load_check.riv; sourceTree = "<group>"; };
E5CD7D7027DC331900BFE5E2 /* SwiftMeshAnimation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftMeshAnimation.swift; sourceTree = "<group>"; };
E5E87A002AE5A83700E7295F /* SwiftVariableFPS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftVariableFPS.swift; sourceTree = "<group>"; };
F2C623352C874E3A0006E0CA /* fallback_fonts.riv */ = {isa = PBXFileReference; lastKnownFileType = file; path = fallback_fonts.riv; sourceTree = "<group>"; };
F2C623382C874E690006E0CA /* SwiftFallbackFonts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftFallbackFonts.swift; sourceTree = "<group>"; };
F8DA7B442AF523A800FF3CBF /* DismissableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissableView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -589,6 +595,7 @@
C9696B0E24FC6FD10041502A /* Assets */ = {
isa = PBXGroup;
children = (
F2C623352C874E3A0006E0CA /* fallback_fonts.riv */,
2E83910C2C050BC4003BCF2A /* runtime_nested_inputs.riv */,
0490915C2BC832D100F2C12B /* lip-sync_test.riv */,
049091522BC832AF00F2C12B /* ping_pong_audio_demo.riv */,
@@ -670,6 +677,7 @@
041265212B0BC5A5009400EC /* SwiftSimpleAssets.swift */,
0490914B2BC8326E00F2C12B /* SwiftAudioAssets.swift */,
049091622BC948AA00F2C12B /* SwiftOutOfBandAudioAssets.swift */,
F2C623382C874E690006E0CA /* SwiftFallbackFonts.swift */,
);
path = SwiftUI;
sourceTree = "<group>";
@@ -993,6 +1001,7 @@
040554212B7A2858008F076A /* Assets.xcassets in Resources */,
040554222B7A2858008F076A /* constrained.riv in Resources */,
040554232B7A2858008F076A /* Bear.riv in Resources */,
F2C623362C874E3A0006E0CA /* fallback_fonts.riv in Resources */,
040554242B7A2858008F076A /* life_bar.riv in Resources */,
040554252B7A2858008F076A /* wacky.riv in Resources */,
040554262B7A2858008F076A /* asset_load_check.riv in Resources */,
@@ -1123,6 +1132,7 @@
C9C73E9E24FC471E00EF9516 /* Assets.xcassets in Resources */,
0450446126B3F71E007B25CA /* constrained.riv in Resources */,
83DE4CB52AB397A800B88B72 /* Bear.riv in Resources */,
F2C623372C874E3A0006E0CA /* fallback_fonts.riv in Resources */,
C9D3DE68264F49F4001BA265 /* life_bar.riv in Resources */,
042C88DB2644447500E7DBB2 /* wacky.riv in Resources */,
E5B5C2182B238829006E57C8 /* asset_load_check.riv in Resources */,
@@ -1167,6 +1177,7 @@
040553DE2B7A2858008F076A /* SwiftSimpleAssets.swift in Sources */,
040553DF2B7A2858008F076A /* StressTest.swift in Sources */,
040553E02B7A2858008F076A /* SwiftVariableFPS.swift in Sources */,
F2C623392C874E690006E0CA /* SwiftFallbackFonts.swift in Sources */,
040553E12B7A2858008F076A /* ExamplesMaster.swift in Sources */,
040553E22B7A2858008F076A /* SwiftTouchEvents.swift in Sources */,
040553E32B7A2858008F076A /* ClockViewModel.swift in Sources */,
@@ -1219,6 +1230,7 @@
041265222B0BC5A5009400EC /* SwiftSimpleAssets.swift in Sources */,
83C89ACB29886ECB00044C17 /* StressTest.swift in Sources */,
E5E87A012AE5A83800E7295F /* SwiftVariableFPS.swift in Sources */,
F2C6233A2C874E690006E0CA /* SwiftFallbackFonts.swift in Sources */,
C3357CA1280F42EC00F03B6F /* ExamplesMaster.swift in Sources */,
C3ECAC272817BE4600A81123 /* SwiftTouchEvents.swift in Sources */,
C3ECAC2F281840A300A81123 /* ClockViewModel.swift in Sources */,

View File

@@ -0,0 +1,55 @@
//
// SwiftFallbackFonts.swift
// RiveExample
//
// Created by David Skuza on 9/3/24.
// Copyright © 2024 Rive. All rights reserved.
//
import SwiftUI
import RiveRuntime
struct SwiftFallbackFonts: View, DismissableView {
var dismiss: () -> Void = {}
@StateObject private var viewModel = RiveViewModel(fileName: "fallback_fonts")
private var runBinding: Binding<String> {
Binding {
return self.viewModel.getTextRunValue("text") ?? ""
}
set: { text in
try? self.viewModel.setTextRunValue("text", textValue: text)
self.viewModel.play()
}
}
var body: some View {
VStack() {
viewModel.view().scaledToFit()
Text(
"The included Rive font only contains characters in the set A...G. Fallback font(s) will be used to draw missing characters."
)
.fixedSize(horizontal: false, vertical: /*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/)
.font(.caption)
.padding()
TextField("Add text with missing characters", text: runBinding)
.textFieldStyle(.roundedBorder)
.padding()
Spacer().frame(maxHeight: .infinity)
}
.onAppear {
RiveFont.fallbackFonts = [
// You can use a font descriptor that will generate a system font
RiveFallbackFontDescriptor(design: .default, weight: .regular, width: .standard),
// ...or an explicit system font
UIFont.systemFont(ofSize: 12, weight: .heavy),
// ...or a UIFont by name, or any way of initializing a UIFont
UIFont(name: "Times New Roman", size: 12)!
]
}
}
}

View File

@@ -44,8 +44,9 @@ class ExamplesMasterTableViewController: UITableViewController {
("Rive Events", typeErased(dismissableView: SwiftEvents())),
("Variable FPS", typeErased(dismissableView: SwiftVariableFPS())),
("Simple Assets", typeErased(dismissableView: SwiftSimpleAssets())),
("Audio Assets", typeErased(dismissableView: SwiftAudioAssets())),
("External Audio Assets", typeErased(dismissableView: SwiftOutOfBandAudioAssets()))
("Audio Assets", typeErased(dismissableView: SwiftAudioAssets())),
("External Audio Assets", typeErased(dismissableView: SwiftOutOfBandAudioAssets())),
("Fallback Fonts", typeErased(dismissableView: SwiftFallbackFonts())),
]