在iOS开发中,使用WKWebView加载包含自定义字体的HTML内容时,开发者经常会遇到字体加载失败的情况。虽然字体文件已经添加到Xcode项目的Bundle中,并且CSS也通过@font-face规则声明了字体路径,但页面渲染时文字仍然回退为系统默认字体。这个问题的核心在于WKWebView加载HTML字符串时,相对路径的解析基准与预期不一致,或者CSS中的字体声明未能在渲染前正确指向本地资源。本文将深入分析背后的原因,并给出三种有效的解决方案:正确设置baseURL、动态注入CSS以及利用本地资源映射。

问题背景与原因分析
WKWebView加载HTML内容通常有两种方式:加载远程URL和加载本地HTML字符串。对于本地HTML字符串,开发者最常用的API是loadHTMLString(_:baseURL:)。这个方法的第二个参数baseURL决定了HTML中所有相对路径资源(包括CSS、图片、字体文件)的解析基准。如果不指定baseURL或传入nil,那么相对路径会以about:blank为基准,此时Bundle内的字体文件自然无法被定位。即使传入了Bundle.main.bundleURL,如果CSS中的路径写法不规范(例如缺少./前缀或使用了错误的目录层级),字体仍然可能加载失败。
另一个容易被忽略的原因是CSS的注入时机。许多开发者选择在webView(_:didFinish:)代理方法中使用evaluateJavaScript动态添加样式,但此时HTML文档已经完成解析和首次渲染,@font-face规则可能来不及生效,导致字体仍然显示为系统默认。此外,WKWebView对本地文件的访问存在沙盒限制,如果HTML内容是从远程加载的,直接引用本地字体文件的路径会被WebKit的安全策略拦截,必须通过本地资源映射来绕开限制。
使用baseURL加载本地资源
最直接的解决方案是在调用loadHTMLString时正确设置baseURL。将baseURL指定为Bundle的根目录,这样HTML中所有相对路径都会基于这个URL进行解析。例如,如果字体文件位于Fonts/MyFont.otf(相对于Bundle根目录),那么CSS中写入url("Fonts/MyFont.otf")即可被正确加载。下面是一个Swift代码示例:
import WebKit
class ViewController: UIViewController, WKWebViewDelegate {
var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
let config = WKWebViewConfiguration()
webView = WKWebView(frame: view.bounds, configuration: config)
webView.navigationDelegate = self
view.addSubview(webView)
// 构建HTML字符串,包含自定义字体声明
let htmlString = """
<!DOCTYPE html>
<html>
<head>
<style>
@font-face {
font-family: 'MyCustomFont';
src: url('Fonts/MyFont.otf') format('opentype');
}
body {
font-family: 'MyCustomFont', sans-serif;
font-size: 24px;
}
</style>
</head>
<body>
<p>这是自定义字体渲染的文本</p>
</body>
</html>
"""
// 关键:baseURL设置为Bundle根目录
webView.loadHTMLString(htmlString, baseURL: Bundle.main.bundleURL)
}
}
使用Bundle.main.bundleURL作为baseURL时,注意路径分隔符要使用正斜杠/,而不是反斜杠\。URL标准要求正斜杠,即使代码运行在iOS(基于Unix)上也不使用Windows路径风格。另外,如果HTML内容是从远程服务器获取的,仅仅设置baseURL是不够的,因为WebKit会限制从远程页面加载本地文件,此时需要配合本地资源映射方案。
还需要注意,如果HTML字符串中使用了绝对路径(如file:///...),那么baseURL的设置不会影响这些绝对路径的解析,开发者需要确保绝对路径本身正确。在实际项目中,建议统一使用相对路径并配合baseURL,这样可以避免因为Bundle路径变化导致的问题。测试时可以在模拟器和真机上分别验证,因为沙盒路径不同,但Bundle.main.bundleURL会自动适配。
通过CSS注入修复字体路径
当HTML内容来自远程服务器或者无法直接修改HTML源码时,可以通过在WKWebView中注入CSS来覆盖原有的@font-face规则。注入的最佳时机是在文档加载的早期,而不是在didFinish之后。利用WKUserScript并设置injectionTime为.atDocumentStart,可以确保CSS在HTML解析之前生效。注入的CSS需要包含正确的本地字体文件路径,通常配合baseURL使用。
下面的代码展示了如何创建一个包含修正字体路径的WKUserScript,并在加载HTML之前将其添加到WKUserContentController中。这样即使HTML中原本的字体路径无效,注入的CSS也会在文档开始时覆盖它。
import WebKit
func createWebViewWithInjectedCSS() -> WKWebView {
let config = WKWebViewConfiguration()
let userContentController = WKUserContentController()
// 要注入的CSS,将字体路径改为Bundle内的相对路径
let cssString = """
@font-face {
font-family: 'MyCustomFont';
src: url('Fonts/MyFont.otf') format('opentype');
}
body {
font-family: 'MyCustomFont', sans-serif !important;
}
"""
// 将CSS包装成JavaScript代码
let jsString = "var style = document.createElement('style'); style.textContent = '\(cssString)'; document.head.appendChild(style);"
let userScript = WKUserScript(source: jsString, injectionTime: .atDocumentStart, forMainFrameOnly: true)
userContentController.addUserScript(userScript)
config.userContentController = userContentController
let webView = WKWebView(frame: .zero, configuration: config)
return webView
}
需要注意的是,注入的CSS中的路径仍然是相对于baseURL的。因此,在调用loadHTMLString时仍然要设置正确的baseURL。如果HTML是从远程加载的,并且baseURL是远程URL,那么本地字体仍然无法访问。这种情况下,可以考虑使用WKURLSchemeHandler将字体资源映射到一个自定义的scheme,然后在注入的CSS中引用该scheme。这种方法既能解决远程内容的限制,又能保持代码的可维护性。
另一种注入CSS的方式是在webView(_:didFinish:)中使用evaluateJavaScript,但正如前文所述,这个时机已经错过首次渲染。不过,如果页面是动态交互的,后续更新字体仍然有效。对于大多数场景,建议使用WKUserScript配合atDocumentStart来确保字体尽早加载。
本地资源映射与进阶方案
当HTML内容来自远程服务器,或者需要加载多个本地资源(字体、图片、CSS等)时,使用WKURLSchemeHandler注册一个自定义scheme是更灵活的方案。通过将自定义scheme(例如custom-font)映射到Bundle中的字体文件,CSS可以引用custom-font://MyFont.otf这样的URL,WebView会调用WKURLSchemeHandler的代理方法来加载资源。这样就不受远程页面安全策略的限制,并且可以统一管理所有本地资源。
实现WKURLSchemeHandler需要两个步骤:注册scheme和实现代理方法。注册scheme必须在创建WKWebViewConfiguration时完成,并且scheme名称不能与内置的http、https等冲突。下面是一个完整的示例:
import WebKit
class FontSchemeHandler: NSObject, WKURLSchemeHandler {
func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
guard let url = urlSchemeTask.request.url,
let fontName = url.host, // 假设scheme格式为 custom-font://MyFont.otf,host是文件名
let fontPath = Bundle.main.path(forResource: fontName, ofType: nil, inDirectory: "Fonts") else {
urlSchemeTask.didFailWithError(NSError(domain: "FontSchemeHandler", code: -1, userInfo: nil))
return
}
let fileURL = URL(fileURLWithPath: fontPath)
do {
let data = try Data(contentsOf: fileURL)
let response = URLResponse(url: url, mimeType: "font/opentype", expectedContentLength: data.count, textEncodingName: nil)
urlSchemeTask.didReceive(response)
urlSchemeTask.didReceive(data)
urlSchemeTask.didFinish()
} catch {
urlSchemeTask.didFailWithError(error)
}
}
func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
// 可以在这里取消加载
}
}
// 在创建WebView时注册scheme
func createWebViewWithCustomScheme() -> WKWebView {
let config = WKWebViewConfiguration()
config.setURLSchemeHandler(FontSchemeHandler(), forURLScheme: "custom-font")
let webView = WKWebView(frame: .zero, configuration: config)
return webView
}
使用自定义scheme后,HTML中的CSS可以写作src: url('custom-font://MyFont.otf')。这种方式非常适合远程HTML,因为WebKit会将custom-font视为自定义协议,不会阻止请求。同时,由于scheme处理器完全在应用沙盒内运行,可以安全地访问Bundle资源。需要注意的是,如果HTML本身是通过http或https加载的,CSS中的自定义scheme请求会被正常发送,不会引发跨域问题,因为WKWebView允许任意scheme的资源加载,只要该scheme已经被注册。
在实际项目中,可能会遇到多个资源类型,比如字体、图片、CSS文件等。可以为每个scheme注册不同的处理器,或者在一个处理器中根据URL路径分发。此外,WKURLSchemeHandler的代理方法需要在主线程中调用,确保数据加载不会阻塞。如果字体文件较大,可以考虑异步读取,但必须保证回调顺序正确。总体而言,本地资源映射方案提供了最大的灵活性和安全性,适合复杂应用场景。