Custom font iOS app’e karakter katıyor. Marka tutarlılığı, differentiated UX. Ama yanlış kullanıldığında performance cost. Dentii’ye custom font eklediğimde launch time 200ms arttı, visible jank ile.
3 ana pitfall buldum ve fix ettim. Launch time’ı restore ettim. Bu yazıda deneyimi paylaşacağım.
Pitfall 1: Too many font files
Bir font genelde birden fazla file’da: Regular, Bold, Italic, Black, Light, Medium, Black Italic, Light Italic. 8 file, her biri 500KB-2MB.
Hepsi binary’e dahil ediliyor. App size artıyor, launch sırasında font system’ine register ediliyor.
Fix: Sadece kullanılan weight’leri include et.
Projende 2 font weight kullanıyorsun (Regular, Bold). Diğerlerini binary’ye ekleme. Asset catalog’a koyma, Info.plist’te UIAppFonts’ta listeleme.
Binary size’dan 3-4MB kazandım. Launch time 80ms kısaldı.
Alternative: Variable fonts. Single file, programmatic weight. GeistVF.ttf (Geist Variable) tek bir dosyada tüm weight’leri barındırıyor.
Text("Hello")
.font(.custom("GeistVF", size: 16))
.fontWeight(.bold) // Automatic variable axis adjustmentModern font’lar variable version sunuyor. Tercih et.
Pitfall 2: Font registration bottleneck
iOS app launch’unda custom font’lar registrasyon sürecinden geçiyor. 5+ font variant için belirgin gecikme.
App startup’ında automatic register:
// Info.plist UIAppFonts array
<key>UIAppFonts</key>
<array>
<string>GeistRegular.otf</string>
<string>GeistBold.otf</string>
</array>Bu automatic registration senkron. Launch time’a eklemiyor.
Alternative: Async registration.
Font’ları Info.plist’e koymayıp programmatically register ediyorsun:
func registerFonts() {
let fontURLs = Bundle.main.urls(forResourcesWithExtension: "otf", subdirectory: nil) ?? []
fontURLs.forEach { url in
var error: Unmanaged<CFError>?
CTFontManagerRegisterFontsForURL(url as CFURL, .process, &error)
}
}
// App launch'unda async
Task.detached(priority: .background) {
registerFonts()
}Background thread’te register. Main thread hızlı.
Ama dikkat: ilk UI render’dan önce font’lar hazır olmalı. Race condition olabilir.
Safer: sadece ilk ekranın font’larını synchronous register, diğerleri async.
Pitfall 3: Font metric mismatches
Default system font (SF Pro, San Francisco) çok well-tuned metrics’e sahip. Custom font’a geçince:
- Line height değişiyor
- Leading (satırlar arası boşluk) farklı
- Cap height başka yerde
- x-height uyumsuz
Result: text UI’da hizasız görünüyor. Her SwiftUI Text component’ı farklı height’ta olabilir.
Fix: Custom UIFont extension.
extension UIFont {
static func geist(size: CGFloat, weight: UIFont.Weight = .regular) -> UIFont {
let fontName: String
switch weight {
case .bold: fontName = "GeistBold"
case .semibold: fontName = "GeistSemiBold"
case .medium: fontName = "GeistMedium"
default: fontName = "GeistRegular"
}
let font = UIFont(name: fontName, size: size) ?? UIFont.systemFont(ofSize: size, weight: weight)
// System font ile eşleştirme için metric override
let metrics = UIFontMetrics.default
return metrics.scaledFont(for: font)
}
}Fallback: custom font load olmamışsa system font.
CSS-like approach with text styles
SwiftUI’da custom font + Dynamic Type entegrasyonu:
extension Font {
static func geistTitle() -> Font {
.custom("GeistBold", size: 28, relativeTo: .title)
}
static func geistBody() -> Font {
.custom("GeistRegular", size: 16, relativeTo: .body)
}
static func geistCaption() -> Font {
.custom("GeistMedium", size: 12, relativeTo: .caption)
}
}relativeTo: parameter kullanıcı’nın text size preference’ına uyum sağlıyor. Custom font + accessibility.
Font subset generation
Büyük project’lerde custom font daha da optimize edilebilir.
Font file’ında tüm Unicode karakter’leri var (Chinese, Arabic, symbols). Kullanmayacağın karakter’leri atıyorsun, font file küçülüyor.
Tool’lar:
– pyftsubset (Python fonttools)
– glyphhanger (Node.js)
pyftsubset GeistRegular.otf --unicodes=U+0020-007F,U+00A0-00FF --output-file=GeistRegular-subset.otfBasic Latin (English + European) için subset. %60-70 size reduction.
Localized app için dikkat: Turkish karakter’leri unicode range’ine dahil et.
FOUT vs FOIT
Web’den gelen problem iOS’a da uyarlanabilir:
- FOIT (Flash of Invisible Text): Font yüklenene kadar text görünmez
- FOUT (Flash of Unstyled Text): Text önce system font ile, sonra custom’a değişir
iOS’ta default FOUT behavior. Text görünüyor, font yüklenince değişiyor. Minor flicker.
Eğer bu flicker rahatsız edici ise:
// Async load font, UI'ı aç sonra
func loadFontThenShowUI() {
CTFontManagerRegisterFontURLs([url], .process, true) { _, _ in
DispatchQueue.main.async {
self.showMainUI() // Font hazır, şimdi UI göster
}
}
}App launch’ı biraz geciktiriyor ama flicker’ı önlüyor.
Licensing implications
Custom font licensing dikkat:
- Google Fonts: free, open source
- Adobe Fonts: Creative Cloud subscription gerekli
- Commercial fonts (Gotham, Proxima Nova): per-platform license
- Custom/bespoke fonts: designer contract
App’e eklenen font’un licensing iOS app için izin veriyor mu? Check et. Some fonts web OK ama native app yok.
Benchmarking
Font loading optimization öncesi/sonrası benchmark:
- App launch time (Instruments)
- First frame render time
- Text rendering perf (scroll bir liste)
Dentii’de:
– Before: 850ms launch (medium phone)
– After: 650ms launch (200ms gain)
Sonuç
Custom font performance impact gerçek ama manage edilebilir. 3 pitfall:
1. Too many font files → sadece kullanılan weight’leri include
2. Font registration overhead → async registration mümkünse
3. Font metric mismatch → UIFontMetrics ile scaled font
Variable fonts modern çözüm. Subset generation size reduction. Proper licensing check.
Custom font marka değeri katıyor. Performance cost’u dikkatli management ile minimize edilebilir.