最小可重现示例(仅使用内置字体):
@main
struct FontExampleApp: App {
@StateObject private var appearanceManager = AppearanceManager()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(appearanceManager)
}
}
}
class AppearanceManager: ObservableObject {
@Published var Font: String = "System" {
didSet {
saveFont()
objectWillChange.send()
print("Set font to \(Font)")
print("Corresponding font name: \(fonts[Font])")
}
}
public let fonts = ["System" : "system-default", // unrecognized name defaults to system font
"Times New Roman" : "TimesNewRomanPSMT",
"Trebuchet" : "TrebuchetMS"
]
init() {
loadFont()
for family in UIFont.familyNames.sorted() {
let names = UIFont.fontNames(forFamilyName: family)
print("Family: \(family) Font names: \(names)")
}
}
private let fontKey = "FontKey"
private func saveFont() {
UserDefaults.standard.set(Font, forKey: fontKey)
}
private func loadFont() {
if let savedFont = UserDefaults.standard.value(forKey: fontKey) as? String {
Font = savedFont
}
}
}
struct ContentView: View {
var body: some View {
VStack {
PostView()
Spacer()
FontSelectionView()
}
.padding()
}
}
struct PostView: View {
@EnvironmentObject var appearanceManager: AppearanceManager
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
Text("Hello World")
// below only works for certain fonts
.font(.custom(appearanceManager.Font, size: 30, relativeTo: .title))
}
.padding()
}
}
}
struct FontSelectionView: View {
@EnvironmentObject var appearanceManager: AppearanceManager
var body: some View {
Section(header: Text("Fonts")) {
VStack(alignment: .leading) {
ForEach(appearanceManager.fonts.keys.sorted(), id: \.self) { fontName in
FontButton(name: fontName)
}
.padding(.vertical, 5)
}
}
}
}
struct FontButton: View {
@EnvironmentObject var appearanceManager: AppearanceManager
let name: String
var isSelected: Bool {
appearanceManager.Font == name
}
var body: some View {
HStack {
Text(name)
.font(.custom(appearanceManager.fonts[name] ?? "", size: 17, relativeTo: .body))
Spacer()
Circle()
.frame(width: 20, height: 20)
.foregroundColor(isSelected ? .secondary : .clear)
.overlay(
Circle()
.stroke(Color.primary, lineWidth: 2)
)
}
.foregroundColor(.secondary)
.onTapGesture {
withAnimation {
appearanceManager.Font = name
}
}
}
}
上面的例子只使用了两种自定义字体,这两种字体都是内置的。请注意,Times New Roman 按预期工作,但 Trebuchet 却没有。
这个最小的可重现示例扩展到下面描述的情况,其中涉及内置字体和第三方字体。
我已将自定义字体添加到我的项目中,并成功地能够使用每种字体来风格化文本,但只能在设置菜单中使用:
class AppearanceManager: ObservableObject {
@Published var Font: String = "System" {
didSet {
saveFont()
objectWillChange.send()
print("Set font to \(Font)")
print("Corresponding font name: \(fonts[Font])")
}
}
public let fonts = ["System" : "system-default", // unrecognized name defaults to system font
"Courier Prime" : "CourierPrime-Regular",
"JetBrains Mono" : "JetBrainsMono-Regular",
"Roboto" : "RobotoMono-Regular",
"Ubuntu" : "UbuntuMono-Regular",
"Victor" : "VictorMono-Regular",
"Times New Roman" : "TimesNewRomanPSMT",
"Trebuchet" : "TrebuchetMS"
]
init() {
loadFont()
// used for debugging
for family in UIFont.familyNames.sorted() {
let names = UIFont.fontNames(forFamilyName: family)
print("Family: \(family) Font names: \(names)")
}
}
private let fontKey = "FontKey"
private func saveFont() {
UserDefaults.standard.set(Font, forKey: fontKey)
}
private func loadFont() {
if let savedFont = UserDefaults.standard.value(forKey: fontKey) as? String {
Font = savedFont
}
}
}
struct SettingsView: View {
@EnvironmentObject var appearanceManager: AppearanceManager
var body: some View {
Form {
FontSelectionView()
}
.navigationTitle("Settings")
}
}
struct FontSelectionView: View {
@EnvironmentObject var appearanceManager: AppearanceManager
var body: some View {
Section(header: Text("Fonts")) {
VStack(alignment: .leading) {
ForEach(appearanceManager.fonts.keys.sorted(), id: \.self) { fontName in
FontButton(name: fontName)
}
.padding(.vertical, 5)
}
}
}
}
struct FontButton: View {
@EnvironmentObject var appearanceManager: AppearanceManager
let name: String
var isSelected: Bool {
appearanceManager.Font == name
}
var body: some View {
HStack {
Text(name)
.font(.custom(appearanceManager.fonts[name] ?? "", size: 17, relativeTo: .body))
Spacer()
Circle()
.frame(width: 20, height: 20)
.foregroundColor(isSelected ? .secondary : .clear)
.overlay(
Circle()
.stroke(Color.primary, lineWidth: 2)
)
}
.foregroundColor(.secondary)
.onTapGesture {
withAnimation {
appearanceManager.Font = name
}
}
}
}
但是,某些字体(不仅是我添加到项目中的自定义字体,还有 Trebuchet 等内置字体)并未应用于整个应用程序中的文本元素:
struct PostView: View {
@EnvironmentObject var appearanceManager: AppearanceManager
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
Text("Hello World")
// below only works for certain fonts
.font(.custom(appearanceManager.Font, size: 30, relativeTo: .title))
}
.padding()
}
}
}
我已将环境对象注入到我的应用程序的根视图中
@main
struct MyApp: App {
@StateObject private var appearanceManager = AppearanceManager()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(appearanceManager)
}
}
}
我还打印了所有可用的字体并确定正在发布实际的字体名称:
// prints in init() method:
...
Family: Times New Roman Font names: ["TimesNewRomanPSMT", "TimesNewRomanPS-ItalicMT", "TimesNewRomanPS-BoldMT", "TimesNewRomanPS-BoldItalicMT"]
Family: Trebuchet MS Font names: ["TrebuchetMS", "TrebuchetMS-Italic", "TrebuchetMS-Bold", "Trebuchet-BoldItalic"]
Family: Ubuntu Mono Font names: ["UbuntuMono-Regular"]
Family: Verdana Font names: ["Verdana", "Verdana-Italic", "Verdana-Bold", "Verdana-BoldItalic"]
Family: Victor Mono Font names: ["VictorMono-Regular"]
...
// prints when I set a font:
Set font to Trebuchet
Corresponding font name: Optional("TrebuchetMS") // Not applied outside of settings menu
Set font to JetBrains Mono
Corresponding font name: Optional("JetBrainsMono-Regular") // works as expected
Set font to Roboto
Corresponding font name: Optional("RobotoMono-Regular") // not applied outside of settings menu
您似乎对
AppearanceManager.Font
应该存储的内容感到困惑。如果它存储实际字体名称(.custom
可以识别的字体名称),则FontButton
中的点击手势不正确:
.onTapGesture {
withAnimation {
appearanceManager.Font = name
}
}
name
这里是按钮在其 Text
中显示的字体的 显示名称。
如果
AppearanceManager.Font
应该存储显示名称,那么 PostView
中的内容不正确:
.font(.custom(appearanceManager.Font, size: 30, relativeTo: .title))
您不应将显示名称传递给
.custom
。
"Times New Roman"
在这里恰好是“工作”,因为它是字体的家族名称,.custom
可以识别它。 TrebuchetMS 的家族名称为“Trebuchet MS”(带有一个额外的空格)。还可以考虑使用姓氏作为选项。大多数(如果不是全部)姓氏都适合显示。
无论如何,您应该决定
AppearanceManager.Font
应存储什么,并使您的代码与之一致。
我建议存储实际的字体名称,并引入一个表示显示名称-字体名称对的结构。
struct FontOption: Identifiable, Hashable{
let fontName: String
let displayName: String
var id: String { fontName }
}
除非您出于某种原因不想立即将字体保存为用户默认值,否则我建议使用
@AppStorage
。 AppearanceManager
看起来像:
class AppearanceManager: ObservableObject {
@AppStorage("FontKey") var font: String = ""
public let fonts = [
FontOption(fontName: "", displayName: "System"),
FontOption(fontName: "TimesNewRomanPSMT", displayName: "Times New Roman"),
FontOption(fontName: "TrebuchetMS", displayName: "Trebuchet"),
]
}
此时,如果
AppearanceManager
没有任何其他成员,我将删除AppearanceManager
并直接在视图中使用@AppStorage
。 fonts
可以只是一个全局常量。
其余代码如下所示:
struct PostView: View {
@EnvironmentObject var appearanceManager: AppearanceManager
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
Text("Hello World")
.font(.custom(appearanceManager.font, size: 30, relativeTo: .title))
}
.padding()
}
}
}
struct FontSelectionView: View {
@EnvironmentObject var appearanceManager: AppearanceManager
var body: some View {
Section(header: Text("Fonts")) {
VStack(alignment: .leading) {
ForEach(appearanceManager.fonts) { font in
FontButton(font: font)
}
.padding(.vertical, 5)
}
}
}
}
struct FontButton: View {
@EnvironmentObject var appearanceManager: AppearanceManager
let font: FontOption
var isSelected: Bool {
appearanceManager.font == font.fontName
}
var body: some View {
HStack {
Text(font.displayName)
.font(.custom(font.fontName, size: 17, relativeTo: .body))
Spacer()
Circle()
.frame(width: 20, height: 20)
.foregroundColor(isSelected ? .secondary : .clear)
.overlay(
Circle()
.stroke(Color.primary, lineWidth: 2)
)
}
.foregroundColor(.secondary)
.onTapGesture {
withAnimation {
appearanceManager.font = font.fontName
}
}
}
}