I have logic that lets the user change Font.Design
, which works fine for SwiftUI views, and also the navigation bar (logic included below). But it does not reload or re-apply to existing TabView
toolbars, a reload is required. What is the best way to update the views live?
@MainActor
private func configureTabBars(with fontDesign: Font.Design) {
UINavigationBar.appearance().largeTitleTextAttributes = [
.font : UIFont(descriptor: UIFontDescriptor.preferredFontDescriptor(withTextStyle: .caption1).withDesign(fontDesign.systemDesign)!, size: 0)
]
UITabBarItem.appearance().setTitleTextAttributes([
.font : UIFont(descriptor: UIFontDescriptor.preferredFontDescriptor(withTextStyle: .caption1).withDesign(fontDesign.systemDesign)!, size: 0)
], for: [])
}
private extension Font.Design {
var systemDesign: UIFontDescriptor.SystemDesign {
switch self {
case .serif: .serif
case .rounded: .rounded
case .monospaced: .monospaced
default: .default
}
}
}
Similar to this question, the reason why appearance()
won't work is that it won't affect views that are already added to the window's view hierarchy.
Similar to the linked question, this can be done by using a UIViewControllerRepresentable
and finding the UINavigationController
and UITabBarItem
from the view controller we control.
struct BarAppearances: UIViewControllerRepresentable {
let fontDesign: Font.Design
// instead of passing a Font.Design through the initialiser like above,
// also consider injecting it into the environment, so you can read it directly like
// @Environment(\.fontDesign) var fontDesign
class VC: UIViewController {
var fontDesign: Font.Design = .default
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
updateBarAppearances()
}
func updateBarAppearances() {
let font = UIFont(descriptor: UIFontDescriptor.preferredFontDescriptor(withTextStyle: .caption1).withDesign(fontDesign.systemDesign)!, size: 0)
// here I am using the new UIBarAppearance APIs, but the old 'largeTitleTextAttributes' should work too
let navAppearance = UINavigationBarAppearance()
navAppearance.configureWithOpaqueBackground()
navAppearance.largeTitleTextAttributes = [.font: font]
for item in tabBarController?.tabBar.items ?? [] {
item.setTitleTextAttributes([.font: font], for: [])
}
navigationController?.navigationBar.standardAppearance = navAppearance
navigationController?.navigationBar.scrollEdgeAppearance = navAppearance
navigationController?.navigationBar.compactAppearance = navAppearance
navigationController?.navigationBar.compactScrollEdgeAppearance = navAppearance
}
}
func makeUIViewController(context: Context) -> VC {
VC()
}
func updateUIViewController(_ uiViewController: VC, context: Context) {
uiViewController.fontDesign = fontDesign
uiViewController.updateBarAppearances()
}
}
To use this, put it as the background
of the root view in the NavigationStack
:
struct ContentView: View {
@State var id = UUID()
@State var design = Font.Design.default
var body: some View {
TabView {
NavigationStack {
VStack {
Button("Set to monospace") {
design = .monospaced
}
Button("Set to rounded") {
design = .rounded
}
}
.navigationTitle("Some Nav Title")
.background { BarAppearances(fontDesign: design) }
}
.tabItem {
Text("Some Tab Title")
}
}
}
}
Another way to solve this is to simply recreate the whole view hierarchy by changing the id
of the TabView
.
Here is a very simple example
@State var id = UUID()
var body: some View {
TabView {
NavigationStack {
VStack {
Button("Set to monospace") {
configureTabBars(with: .monospaced)
id = UUID()
}
Button("Set to rounded") {
configureTabBars(with: .rounded)
id = UUID()
}
}
.navigationTitle("Some Nav Title")
}
.tabItem {
Text("Some Tab Title")
}
}
.id(id)
}
The problem with this is that all the @State
s of the individual tabs will be lost. Though, since this is changing the app's theme, I personally wouldn't mind, as a user.