fix: Replace local() with url() for WKWebView font loading
Fix custom fonts not displaying in WKWebView by replacing CSS `src: local()` with `src: url()` using bundle file paths. WKWebView cannot resolve fonts via local() even when registered in Info.plist UIAppFonts. Changes: - Add fontFileNames and cssFontFamily properties to FontFamily enum - Implement generateFontFaceCSS() helper in WebView.swift and NativeWebView.swift - Generate @font-face CSS with file:// URLs for all 12 font files - Fonts are now dynamically loaded from FontFamily enum (single source of truth) Affected fonts: - Literata (Regular, Bold) - Merriweather (Regular, Bold) - Source Serif 4 (Regular, Bold) - Lato (Regular, Bold) - Montserrat (Regular, Bold) - Source Sans 3 (Regular, Bold)
This commit is contained in:
parent
e96d6e13ac
commit
997d740597
@ -69,6 +69,61 @@ enum FontFamily: String, CaseIterable {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the font file names (without extension) for this font family
|
||||
/// These correspond to the files listed in Info.plist under UIAppFonts
|
||||
var fontFileNames: [(fileName: String, weight: String)]? {
|
||||
switch self {
|
||||
case .literata:
|
||||
return [
|
||||
("Literata-Regular", "normal"),
|
||||
("Literata-Bold", "bold")
|
||||
]
|
||||
case .merriweather:
|
||||
return [
|
||||
("Merriweather-Regular", "normal"),
|
||||
("Merriweather-Bold", "bold")
|
||||
]
|
||||
case .sourceSerif:
|
||||
return [
|
||||
("SourceSerif4-Regular", "normal"),
|
||||
("SourceSerif4-Bold", "bold")
|
||||
]
|
||||
case .lato:
|
||||
return [
|
||||
("Lato-Regular", "normal"),
|
||||
("Lato-Bold", "bold")
|
||||
]
|
||||
case .montserrat:
|
||||
return [
|
||||
("Montserrat-Regular", "normal"),
|
||||
("Montserrat-Bold", "bold")
|
||||
]
|
||||
case .sourceSans:
|
||||
return [
|
||||
("SourceSans3-Regular", "normal"),
|
||||
("SourceSans3-Bold", "bold")
|
||||
]
|
||||
// System fonts don't need to be loaded via @font-face
|
||||
case .system, .newYork, .avenirNext, .monospace, .serif, .sansSerif:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the CSS font-family name used in @font-face declarations
|
||||
var cssFontFamily: String? {
|
||||
switch self {
|
||||
case .literata: return "Literata"
|
||||
case .merriweather: return "Merriweather"
|
||||
case .sourceSerif: return "Source Serif 4"
|
||||
case .lato: return "Lato"
|
||||
case .montserrat: return "Montserrat"
|
||||
case .sourceSans: return "Source Sans 3"
|
||||
// System fonts don't need CSS font-family names
|
||||
case .system, .newYork, .avenirNext, .monospace, .serif, .sansSerif:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum FontCategory {
|
||||
|
||||
@ -190,66 +190,7 @@ struct NativeWebView: View {
|
||||
<meta name="color-scheme" content="\(isDarkMode ? "dark" : "light")">
|
||||
<style>
|
||||
/* Load custom fonts from app bundle */
|
||||
@font-face {
|
||||
font-family: 'Literata';
|
||||
src: local('Literata-Regular');
|
||||
font-weight: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Literata';
|
||||
src: local('Literata-Bold');
|
||||
font-weight: bold;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Merriweather';
|
||||
src: local('Merriweather-Regular');
|
||||
font-weight: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Merriweather';
|
||||
src: local('Merriweather-Bold');
|
||||
font-weight: bold;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Source Serif 4';
|
||||
src: local('SourceSerif4-Regular');
|
||||
font-weight: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Source Serif 4';
|
||||
src: local('SourceSerif4-Bold');
|
||||
font-weight: bold;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Lato';
|
||||
src: local('Lato-Regular');
|
||||
font-weight: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Lato';
|
||||
src: local('Lato-Bold');
|
||||
font-weight: bold;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Montserrat';
|
||||
src: local('Montserrat-Regular');
|
||||
font-weight: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Montserrat';
|
||||
src: local('Montserrat-Bold');
|
||||
font-weight: bold;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Source Sans 3';
|
||||
src: local('SourceSans3-Regular');
|
||||
font-weight: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Source Sans 3';
|
||||
src: local('SourceSans3-Bold');
|
||||
font-weight: bold;
|
||||
}
|
||||
\(generateFontFaceCSS())
|
||||
|
||||
* {
|
||||
max-width: 100%;
|
||||
@ -445,6 +386,36 @@ struct NativeWebView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func generateFontFaceCSS() -> String {
|
||||
var css = ""
|
||||
|
||||
// Iterate through all font families from the enum
|
||||
for fontFamily in FontFamily.allCases {
|
||||
// Only process fonts that need to be loaded (Google fonts)
|
||||
guard let fileNames = fontFamily.fontFileNames,
|
||||
let cssFamilyName = fontFamily.cssFontFamily else {
|
||||
continue
|
||||
}
|
||||
|
||||
// Generate @font-face rules for each weight variant
|
||||
for (fileName, weight) in fileNames {
|
||||
if let fontPath = Bundle.main.path(forResource: fileName, ofType: "ttf") {
|
||||
let fileURL = URL(fileURLWithPath: fontPath).absoluteString
|
||||
css += """
|
||||
@font-face {
|
||||
font-family: '\(cssFamilyName)';
|
||||
src: url('\(fileURL)') format('truetype');
|
||||
font-weight: \(weight);
|
||||
}
|
||||
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return css
|
||||
}
|
||||
|
||||
private func getFontSize(from fontSize: FontSize) -> Int {
|
||||
switch fontSize {
|
||||
case .small: return 14
|
||||
|
||||
@ -75,66 +75,7 @@ struct WebView: UIViewRepresentable {
|
||||
<meta name="color-scheme" content="\(isDarkMode ? "dark" : "light")">
|
||||
<style>
|
||||
/* Load custom fonts from app bundle */
|
||||
@font-face {
|
||||
font-family: 'Literata';
|
||||
src: local('Literata-Regular');
|
||||
font-weight: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Literata';
|
||||
src: local('Literata-Bold');
|
||||
font-weight: bold;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Merriweather';
|
||||
src: local('Merriweather-Regular');
|
||||
font-weight: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Merriweather';
|
||||
src: local('Merriweather-Bold');
|
||||
font-weight: bold;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Source Serif 4';
|
||||
src: local('SourceSerif4-Regular');
|
||||
font-weight: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Source Serif 4';
|
||||
src: local('SourceSerif4-Bold');
|
||||
font-weight: bold;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Lato';
|
||||
src: local('Lato-Regular');
|
||||
font-weight: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Lato';
|
||||
src: local('Lato-Bold');
|
||||
font-weight: bold;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Montserrat';
|
||||
src: local('Montserrat-Regular');
|
||||
font-weight: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Montserrat';
|
||||
src: local('Montserrat-Bold');
|
||||
font-weight: bold;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Source Sans 3';
|
||||
src: local('SourceSans3-Regular');
|
||||
font-weight: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Source Sans 3';
|
||||
src: local('SourceSans3-Bold');
|
||||
font-weight: bold;
|
||||
}
|
||||
\(generateFontFaceCSS())
|
||||
|
||||
:root {
|
||||
--background-color: \(isDarkMode ? "#000000" : "#ffffff");
|
||||
@ -407,6 +348,36 @@ struct WebView: UIViewRepresentable {
|
||||
WebViewCoordinator()
|
||||
}
|
||||
|
||||
private func generateFontFaceCSS() -> String {
|
||||
var css = ""
|
||||
|
||||
// Iterate through all font families from the enum
|
||||
for fontFamily in FontFamily.allCases {
|
||||
// Only process fonts that need to be loaded (Google fonts)
|
||||
guard let fileNames = fontFamily.fontFileNames,
|
||||
let cssFamilyName = fontFamily.cssFontFamily else {
|
||||
continue
|
||||
}
|
||||
|
||||
// Generate @font-face rules for each weight variant
|
||||
for (fileName, weight) in fileNames {
|
||||
if let fontPath = Bundle.main.path(forResource: fileName, ofType: "ttf") {
|
||||
let fileURL = URL(fileURLWithPath: fontPath).absoluteString
|
||||
css += """
|
||||
@font-face {
|
||||
font-family: '\(cssFamilyName)';
|
||||
src: url('\(fileURL)') format('truetype');
|
||||
font-weight: \(weight);
|
||||
}
|
||||
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return css
|
||||
}
|
||||
|
||||
private func getFontSize(from fontSize: FontSize) -> Int {
|
||||
switch fontSize {
|
||||
case .small: return 14
|
||||
|
||||
@ -1,145 +0,0 @@
|
||||
//
|
||||
// LogStore.swift
|
||||
// readeck
|
||||
//
|
||||
// Created by Ilyas Hallak on 01.11.25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - Log Entry
|
||||
|
||||
struct LogEntry: Identifiable, Codable {
|
||||
let id: UUID
|
||||
let timestamp: Date
|
||||
let level: LogLevel
|
||||
let category: LogCategory
|
||||
let message: String
|
||||
let file: String
|
||||
let function: String
|
||||
let line: Int
|
||||
|
||||
var fileName: String {
|
||||
URL(fileURLWithPath: file).lastPathComponent.replacingOccurrences(of: ".swift", with: "")
|
||||
}
|
||||
|
||||
var formattedTimestamp: String {
|
||||
DateFormatter.logTimestamp.string(from: timestamp)
|
||||
}
|
||||
|
||||
init(
|
||||
id: UUID = UUID(),
|
||||
timestamp: Date = Date(),
|
||||
level: LogLevel,
|
||||
category: LogCategory,
|
||||
message: String,
|
||||
file: String,
|
||||
function: String,
|
||||
line: Int
|
||||
) {
|
||||
self.id = id
|
||||
self.timestamp = timestamp
|
||||
self.level = level
|
||||
self.category = category
|
||||
self.message = message
|
||||
self.file = file
|
||||
self.function = function
|
||||
self.line = line
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Log Store
|
||||
|
||||
actor LogStore {
|
||||
static let shared = LogStore()
|
||||
|
||||
private var entries: [LogEntry] = []
|
||||
private let maxEntries: Int
|
||||
|
||||
private init(maxEntries: Int = 1000) {
|
||||
self.maxEntries = maxEntries
|
||||
}
|
||||
|
||||
func addEntry(_ entry: LogEntry) {
|
||||
entries.append(entry)
|
||||
|
||||
// Keep only the most recent entries
|
||||
if entries.count > maxEntries {
|
||||
entries.removeFirst(entries.count - maxEntries)
|
||||
}
|
||||
}
|
||||
|
||||
func getEntries() -> [LogEntry] {
|
||||
return entries
|
||||
}
|
||||
|
||||
func getEntries(
|
||||
level: LogLevel? = nil,
|
||||
category: LogCategory? = nil,
|
||||
searchText: String? = nil
|
||||
) -> [LogEntry] {
|
||||
var filtered = entries
|
||||
|
||||
if let level = level {
|
||||
filtered = filtered.filter { $0.level == level }
|
||||
}
|
||||
|
||||
if let category = category {
|
||||
filtered = filtered.filter { $0.category == category }
|
||||
}
|
||||
|
||||
if let searchText = searchText, !searchText.isEmpty {
|
||||
filtered = filtered.filter {
|
||||
$0.message.localizedCaseInsensitiveContains(searchText) ||
|
||||
$0.fileName.localizedCaseInsensitiveContains(searchText) ||
|
||||
$0.function.localizedCaseInsensitiveContains(searchText)
|
||||
}
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
func clear() {
|
||||
entries.removeAll()
|
||||
}
|
||||
|
||||
func exportAsText() -> String {
|
||||
var text = "Readeck Debug Logs\n"
|
||||
text += "Generated: \(DateFormatter.exportTimestamp.string(from: Date()))\n"
|
||||
text += "Total Entries: \(entries.count)\n"
|
||||
text += String(repeating: "=", count: 80) + "\n\n"
|
||||
|
||||
for entry in entries {
|
||||
text += "[\(entry.formattedTimestamp)] "
|
||||
text += "[\(entry.level.emoji) \(levelName(for: entry.level))] "
|
||||
text += "[\(entry.category.rawValue)] "
|
||||
text += "\(entry.fileName):\(entry.line) "
|
||||
text += "\(entry.function)\n"
|
||||
text += " \(entry.message)\n\n"
|
||||
}
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
private func levelName(for level: LogLevel) -> String {
|
||||
switch level.rawValue {
|
||||
case 0: return "DEBUG"
|
||||
case 1: return "INFO"
|
||||
case 2: return "NOTICE"
|
||||
case 3: return "WARNING"
|
||||
case 4: return "ERROR"
|
||||
case 5: return "CRITICAL"
|
||||
default: return "UNKNOWN"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - DateFormatter Extension
|
||||
|
||||
extension DateFormatter {
|
||||
static let exportTimestamp: DateFormatter = {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||
return formatter
|
||||
}()
|
||||
}
|
||||
@ -1,38 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
class VersionManager {
|
||||
static let shared = VersionManager()
|
||||
|
||||
private let lastSeenVersionKey = "lastSeenAppVersion"
|
||||
private let userDefaults = UserDefaults.standard
|
||||
|
||||
var currentVersion: String {
|
||||
Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "1.0"
|
||||
}
|
||||
|
||||
var currentBuild: String {
|
||||
Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "1"
|
||||
}
|
||||
|
||||
var fullVersion: String {
|
||||
"\(currentVersion) (\(currentBuild))"
|
||||
}
|
||||
|
||||
var lastSeenVersion: String? {
|
||||
userDefaults.string(forKey: lastSeenVersionKey)
|
||||
}
|
||||
|
||||
var isNewVersion: Bool {
|
||||
guard let lastSeen = lastSeenVersion else {
|
||||
// First launch
|
||||
return true
|
||||
}
|
||||
return lastSeen != currentVersion
|
||||
}
|
||||
|
||||
func markVersionAsSeen() {
|
||||
userDefaults.set(currentVersion, forKey: lastSeenVersionKey)
|
||||
}
|
||||
|
||||
private init() {}
|
||||
}
|
||||
@ -302,15 +302,3 @@ extension Dictionary {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Debug Build Detection
|
||||
|
||||
extension Bundle {
|
||||
var isDebugBuild: Bool {
|
||||
#if DEBUG
|
||||
return true
|
||||
#else
|
||||
return false
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user