在 SwiftUI 中为 MacOS 创建谷歌字体浏览器,了解字体如何在较低级别工作,并了解它如何与 SwiftUI 结合

Google Fonts是设计用户界面时使用的免费字体的首选网站。本教程将展示如何编写一个简单的工具来预览这些字体,而无需在系统中注册每种字体。

该应用程序包含一个拆分视图,该视图在左侧面板中包含字体列表。右侧面板将显示字体样式选项的预览。

在这里插入图片描述

项目设置

  • 创建一个新的 Mac SwiftUI 项目,命名为GoogleFontPrevew
  • 在 App Sandbox 中启用传出连接(客户端)
  • 从Google Developer Console获取 Google API 。

谷歌字体模型

Google Fonts API 列出了Google Fonts中可用的所有字体。使用Google Fonts API检索字体。响应包含所有样式和用于访问样式字体文件的 URL。缺少元数据,如创建者和描述。但是,我们已经足够创建一个简单的预览,这就是我们的目标。

{
  "kind": "webfonts#webfontList",
  "items": [
    {
      "family": "ABeeZee",
      "variants": [
        "regular",
        "italic"
      ],
      "subsets": [
        "latin",
        "latin-ext"
      ],
      "version": "v22",
      "lastModified": "2022-09-22",
      "files": {
        "regular": "http://fonts.gstatic.com/s/abeezee/v22/esDR31xSG-6AGleN6tKukbcHCpE.ttf",
        "italic": "http://fonts.gstatic.com/s/abeezee/v22/esDT31xSG-6AGleN2tCklZUCGpG-GQ.ttf"
      },
      "category": "sans-serif",
      "kind": "webfonts#webfont"
    }
  ]
}

谷歌字体模型

创建一个GoogleFont.swift文件。该文件将包含我们将用作视图模型的数据结构。

创建 API JSON 响应顶级的 1:1 映射。

struct  GoogleResponse : Decodable { 
  let kind: String 
  let items: [ GoogleFont ] 
}

创建 GoogleFont 结构来表示字体条目。这将ContentView用于显示字体列表。它主要是响应 JSON 中字体项的 1:1 映射。这些文件将从 JSON 中的字典映射到排序数组。

为了可用,字体需要是Decodable,Hashable和Identifiable。由于每个条目的姓氏都是唯一的,因此它可以用作对象的唯一标识符。

struct  GoogleFont : Hashable , Identifiable , Decodable { 
  var id : String { family } 

  let family: String 
  let files: [ GoogleFontStyle ] 
  let version: String 
  let category: String
 }
 ```
 FontFile是将在详细信息显示中用于预览字体系列样式的内容。字体的样式和 url 将被存储以便于参考。为了能够在列表中使用样式,它需要是Hashableand Identifiable。将生成一个 UUID 以唯一标识字体文件。

struct FontFile : Hashable , Identifiable {
let style: Style
let id = UUID ()
let url: URL
}

 创建一个Style.swift文件。这是Style定义枚举的地方。Google 将权重定义为 100–900。italic如果权重有斜体变体,则附加到值。然而,对于普通斜体和普通斜体,谷歌只是分别使用regular和italic。

枚举有一个帮助函数,可以从样式中获取一个友好的名称。友好名称将用作包装样式预览的组框的标题。

enum Style: String, Hashable {
case thin = "100"
case thinItalic = "100italic"
case extraLight = "200"
case extraLightItalic = "200italic"
case light = "300"
case lightItalic = "300italic"
case normal = "regular"
case normalItalic = "italic"
case medium = "500"
case mediumItalic = "500italic"
case semiBold = "600"
case semiBoldItalic = "600italic"
case bold = "700"
case boldItalic = "700italic"
case extraBold = "800"
case extraBoldItalic = "800italic"
case black = "900"
case blackItalic = "900italic"

var friendlyName: String {
switch self {
case .thin: return "Thin"
case .thinItalic: return "Thin Italic"
case .extraLight: return "Extra Light"
case .extraLightItalic: return "Extra Light Italic"
case .light: return "Light"
case .lightItalic: return "Light Italic"
case .normal: return "Normal"
case .normalItalic: return "Italic"
case .medium: return "Medium"
case .mediumItalic: return "Medium Italic"
case .semiBold: return "Semi Bold"
case .semiBoldItalic: return "Semi Bold Italic"
case .bold: return "Bold"
case .boldItalic: return "Bold Italic"
case .extraBold: return "Extra Bold"
case .extraBoldItalic: return "Extra Bold Italic"
case .black: return "Black"
case .blackItalic: return "Black Italic"
}
}
}

现在,有了这一切,自定义解码就可以在GoogleFont. 添加Decodable's所需的初始化器和编码键。Xcode 14 的好处在于它会为您自动填充其中的一些内容。

struct GoogleFont: Hashable, Decodable, Identifiable {
var id: String { family }

let family: String
let files: [FontFile]
let version: String
let category: String

enum CodingKeys: CodingKey {
case family
case files
case version
case category
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.family = try container.decode(String.self, forKey: .family)
let files = try container.decode([String: String].self, forKey: .files)
self.version = try container.decode(String.self, forKey: .version)
self.category = try container.decode(String.self, forKey: .category)

var keys = Array(files.keys)

// lets do a bit of work to get our files that we want.
keys.sort { item1, item2 in
  func itemFixer(_ value: String) -> String {
    if value == "regular" {
      return "400"
    } else if value == "italic" {
      return "400Italic"
    }

    return value
  }

  let fixedItem1 = itemFixer(item1)
  let fixedItem2 = itemFixer(item2)

  return fixedItem1 < fixedItem2
}

self.files = keys.compactMap { key in
  guard let style = Style(rawValue: key),
        let location = files[key]?.replacingOccurrences(of: "http", with: "https")
  else {
    return nil
  }

  return FontFile(style: style, url: URL(string: location)!)
}

}
}

直接从字体容器解码family, version, 。category
将文件映射从 JSON 解码为临时变量。
获取密钥并按照从最亮 (100) 到最暗 (900) 的顺序对它们进行排序。块中有一个小的局部函数,它接受regular并将其映射到400,并将italic其映射到400italic。这样做可以让普通字体和斜体字体排在中等 (500) 之前和浅色 (300) 之后。
对键进行排序后,使用compactMap映射创建排序FontFile数组。使用compactMap允许nil在创建文件数组时忽略条目。如果map被使用,一个nil条目将被插入到数组中。
将 http 替换为 https,这样应用程序就不会抱怨使用不安全的网络调用。

# 谷歌字体服务
创建一个GoogleFontService.swift文件。该文件将包含一个类,该类对Google Fonts API进行网络调用以检索应用程序中显示的字体列表。

class GoogleFontService {
let apiKey = " YOUR KEY HERE "

func syncFonts() async throws -> [GoogleFont] {
var components = URLComponents()

components.scheme = "https"
components.host = "www.googleapis.com"
components.path = "/webfonts/v1/webfonts"
components.queryItems = [
  URLQueryItem(name: "key", value: apiKey),
  URLQueryItem(name: "sort", value: "alpha")
]

let url = components.url!
let request = URLRequest(url: url)

let (data, _) = try await URLSession.shared.data(for: request)
let googleResponse = try JSONDecoder()
  .decode(GoogleResponse.self, from: data)

return googleResponse.items

}
}


将 替换为apiKey从 Google API 控制台检索到的值。
使用 URLComponents 构建 API 的 URL。这保证了 URL 格式正确。
要求 API 按字母顺序对列表进行排序。
调用谷歌字体 API
返回解码后的 GoogleFont 项。
如果没有返回值,请确保:

# Google API 密钥正确
为应用程序启用传出连接。
创建可用的网络字体
要创建可用的网络字体,将混合使用 Core Graphics 和 Core Text。Core Graphics 是一个低级框架,用于管理绘制 2D 对象,包括文本。另一方面,Core Text 是低级文本渲染和布局引擎,SwiftUI 使用它来渲染文本和处理字体。

SwiftUIFont有一个构造函数,它接受一个CTFont(Core Text font) 并返回一个 SwiftUI Font。register 函数将下载字体文件并进入 SwiftUI Font。创建一个Font+Register.swift文件以放入扩展代码。

extension Font {
static func register(url: URL, size: CGFloat = 18) async throws -> Font? {
do {
let request = URLRequest(url: url)
let (data, _) = try await URLSession.shared.data(for: request)

  guard let provider = CGDataProvider(data: data as CFData),
        let cgFont = CGFont(provider)
  else {
    print("Unsucessfully registered font")
    return nil
  }

  let ctFont = CTFontCreateWithGraphicsFont(cgFont, size, nil, nil)

  return Font(ctFont)
} catch {
  print(error)
}

return nil

}
}


创建一个状态变量来保存GoogleFont项目列表。
创建一个状态变量来保存当前选择的GoogleFont
创建具有两列的导航拆分视图。
添加FontList到第一个块(左侧)。请注意,选择状态绑定到FontList's选择。
如果列表不为空,请将详细信息设置为DetailsView. 否则,显示一个EmptyView
添加一个task修饰符,用于检索字体并将当前选择设置为列表中的第一个字体。

# 字体列表
字体列表显示所有可用字体并更新当前选择的字体。创建一个FontList.swift文件。

struct FontList : View {
@Binding var selectedFont: GoogleFont ?
var fonts: [ GoogleFont ]

var body: some View {
List (fonts, selection: $selectedFont ) { NavigationLink中的字体(value: font) { VStack (alignment: .leading) { Text (font.family) .font(.system (大小:14,粗细:.bold))文本(“ \(font.files.count)样式”)

        .font(.system(size: 14 )) 
    } 
    .padding( 8 ) 
  } 
} 

}
}


创建绑定以更新当前选定的字体。
创建一个变量来保存要显示的字体。
创建列表视图以显示字体并更新选择。
这些单元格将是一个简单的垂直堆栈,显示字体系列的名称和样式数。
添加少量填充以增加视觉吸引力。

# 详细视图
显示DetailsView字体系列中每种样式的预览。样式将显示在惰性垂直堆栈中。创建一个DetailView.swift

struct DetailsView: View {
var font: GoogleFont

var body: some View {
VStack {
Text(font.family)
.font(.system(size: 25, weight: .bold))
Spacer()
ScrollView {
LazyVStack(alignment: .leading) {
ForEach(font.files, id: .id) { value in
StyleCell(style: value)
}
}
}
.id(font.family)
}
.padding()
}
}


将预览包装在一个以样式friendlyName作为标题的组框中。
如果有已注册的字体,则显示预览。否则,显示“Trying to load”。确保这些显示在水平允许的最大空间内。
在task修改器中,在创建视图时注册样式的字体。
现在运行该项目允许用户选择一种字体并显示其预览。

# 项目文件

https://github.com/scottandrew/GoogleFontPreview

发表回复