diff --git a/ios/HackerNews/Auth/LoginRow.swift b/ios/HackerNews/Auth/LoginRow.swift
index 19c7c2b8..645c7956 100644
--- a/ios/HackerNews/Auth/LoginRow.swift
+++ b/ios/HackerNews/Auth/LoginRow.swift
@@ -7,11 +7,13 @@
import Foundation
import SwiftUI
+import Common
struct LoginRow: View {
let loggedIn: Bool
let tapped: () -> Void
-
+ @Environment(Theme.self) private var theme
+
var body: some View {
HStack(alignment: .center, spacing: 8) {
Circle()
@@ -20,7 +22,7 @@ struct LoginRow: View {
width: 8
)
Text(loginText())
- .font(.ibmPlexMono(.bold, size: 16))
+ .font(theme.themedFont(size: 16, style: .mono, weight: .bold))
Spacer()
Image(systemName: "message.fill")
.font(.system(size: 12))
@@ -57,4 +59,5 @@ struct LoginRow: View {
#Preview {
LoginRow(loggedIn: true, tapped: {})
+ .environment(Theme())
}
diff --git a/ios/HackerNews/HNApp.swift b/ios/HackerNews/HNApp.swift
index dc1d01e7..7f633baf 100644
--- a/ios/HackerNews/HNApp.swift
+++ b/ios/HackerNews/HNApp.swift
@@ -27,7 +27,6 @@ struct HackerNewsApp: App {
SentrySDK.start { options in
options.dsn =
"https://118cff4b239bd3e0ede8fd74aad9bf8f@o497846.ingest.sentry.io/4506027753668608"
- options.enableTracing = true
options.configureUserFeedback = { config in
config.onSubmitSuccess = { data in
print("Feedback submitted successfully: \(data)")
diff --git a/ios/HackerNews/Hacker-News-Info.plist b/ios/HackerNews/Hacker-News-Info.plist
index e4aa7019..bf0cdc4a 100644
--- a/ios/HackerNews/Hacker-News-Info.plist
+++ b/ios/HackerNews/Hacker-News-Info.plist
@@ -19,9 +19,12 @@
UIAppFonts
- Fonts_Fonts.bundle/ibm_plex_sans_regular.ttf
- Fonts_Fonts.bundle/ibm_plex_sans_medium.ttf
- Fonts_Fonts.bundle/ibm_plex_sans_bold.ttf
+ Fonts_Fonts.bundle/IBMPlexSans-Regular.ttf
+ Fonts_Fonts.bundle/IBMPlexSans-Medium.ttf
+ Fonts_Fonts.bundle/IBMPlexSans-Bold.ttf
+ Fonts_Fonts.bundle/IBMPlexMono-Bold.ttf
+ Fonts_Fonts.bundle/IBMPlexMono-Medium.ttf
+ Fonts_Fonts.bundle/IBMPlexMono-Regular.ttf
diff --git a/ios/HackerNews/Settings/SettingsRow.swift b/ios/HackerNews/Settings/SettingsRow.swift
index 109bc1d3..f73b6012 100644
--- a/ios/HackerNews/Settings/SettingsRow.swift
+++ b/ios/HackerNews/Settings/SettingsRow.swift
@@ -7,18 +7,20 @@
import Foundation
import SwiftUI
+import Common
struct SettingsRow: View {
let text: String
@ViewBuilder let leadingIcon: () -> Leading
@ViewBuilder let trailingIcon: () -> Trailing
let action: () -> Void
+ @Environment(Theme.self) private var theme
var body: some View {
HStack(alignment: .center, spacing: 8) {
leadingIcon()
Text(text)
- .font(.ibmPlexMono(.bold, size: 16))
+ .font(theme.themedFont(size: 16, style: .mono, weight: .bold))
Spacer()
trailingIcon()
}
@@ -45,4 +47,5 @@ struct SettingsRow: View {
},
action: {}
)
+ .environment(Theme())
}
diff --git a/ios/HackerNews/Settings/SettingsScreen.swift b/ios/HackerNews/Settings/SettingsScreen.swift
index 8c11b2f3..4e6f7193 100644
--- a/ios/HackerNews/Settings/SettingsScreen.swift
+++ b/ios/HackerNews/Settings/SettingsScreen.swift
@@ -19,7 +19,7 @@ struct SettingsScreen: View {
LazyVStack(spacing: 8) {
VStack(alignment: .leading, spacing: 4) {
Text("Profile")
- .font(.ibmPlexSans(.medium, size: 12))
+ .font(theme.themedFont(size: 12, style: .sans, weight: .medium))
LoginRow(loggedIn: model.authState == AuthState.loggedIn) {
model.gotoLogin()
}
@@ -27,7 +27,7 @@ struct SettingsScreen: View {
VStack(alignment: .leading, spacing: 4) {
Text("About")
- .font(.ibmPlexSans(.medium, size: 12))
+ .font(theme.themedFont(size: 12, style: .sans, weight: .medium))
SettingsRow(
text: "Follow Emerge",
leadingIcon: {
@@ -132,31 +132,63 @@ struct SettingsScreen: View {
@Bindable var theme = theme
Text("Appearance")
.font(.ibmPlexSans(.medium, size: 12))
-
+
SettingsRow(
- text: "Use System Font",
+ text: "Font Family",
leadingIcon: {
Image(systemName: "textformat")
.font(.system(size: 12))
.foregroundStyle(.purple)
},
trailingIcon: {
- Toggle("", isOn: $theme.useSystemFont)
- .labelsHidden()
+ Menu {
+ Button("System") {
+ theme.fontFamilyPreference = .system
+ }
+ Button("IBM Plex") {
+ theme.fontFamilyPreference = .ibmPlex
+ }
+ } label: {
+ HStack(spacing: 4) {
+ Text(
+ theme.fontFamilyPreference.displayName
+ )
+ .font(theme.themedFont(size: 14, style: .mono))
+ Image(systemName: "chevron.down")
+ .font(.system(size: 12))
+ }
+ .foregroundStyle(.onBackground)
+ }
},
action: {}
)
-
+
SettingsRow(
- text: "Use Monospaced Font",
+ text: "Font Style",
leadingIcon: {
- Image(systemName: "textformat.size")
+ Image(systemName: "textformat")
.font(.system(size: 12))
- .foregroundStyle(.orange)
+ .foregroundStyle(.purple)
},
trailingIcon: {
- Toggle("", isOn: $theme.useMonospaced)
- .labelsHidden()
+ Menu {
+ Button("Sans") {
+ theme.fontStylePreference = .sans
+ }
+ Button("Sans + Mono") {
+ theme.fontStylePreference = .sansAndMono
+ }
+ } label: {
+ HStack(spacing: 4) {
+ Text(
+ theme.fontStylePreference.displayName
+ )
+ .font(theme.themedFont(size: 14, style: .mono))
+ Image(systemName: "chevron.down")
+ .font(.system(size: 12))
+ }
+ .foregroundStyle(.onBackground)
+ }
},
action: {}
)
diff --git a/ios/HackerNews/Stories/StoryRow.swift b/ios/HackerNews/Stories/StoryRow.swift
index 2e2dc498..715d2e57 100644
--- a/ios/HackerNews/Stories/StoryRow.swift
+++ b/ios/HackerNews/Stories/StoryRow.swift
@@ -42,7 +42,7 @@ struct StoryRow: View {
let author = content.author!
HStack {
Text("@\(author)")
- .font(theme.userMonoFont(size: 12, weight: .bold))
+ .font(theme.themedFont(size: 12, style: .mono, weight: .bold))
.foregroundColor(.hnOrange)
Spacer()
if content.bookmarked {
@@ -62,14 +62,14 @@ struct StoryRow: View {
.font(.system(size: 12))
.foregroundColor(.green)
Text("\(content.score)")
- .font(theme.userSansFont(size: 12, weight: .medium))
+ .font(theme.themedFont(size: 12, style: .sans, weight: .medium))
}
HStack(spacing: 4) {
Image(systemName: "clock")
.font(.system(size: 12))
.foregroundColor(.purple)
Text(content.relativeDate())
- .font(theme.userSansFont(size: 12, weight: .medium))
+ .font(theme.themedFont(size: 12, style: .sans, weight: .medium))
}
Spacer()
// Comment Button
@@ -84,7 +84,7 @@ struct StoryRow: View {
Image(systemName: "message.fill")
.font(.system(size: 12))
Text("\(content.commentCount)")
- .font(theme.userSansFont(size: 12, weight: .medium))
+ .font(theme.themedFont(size: 12, style: .sans, weight: .medium))
}
.foregroundStyle(.blue)
}
@@ -104,11 +104,11 @@ struct StoryRowLoadingState: View {
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text("@humdinger")
- .font(theme.userMonoFont(size: 12, weight: .bold))
+ .font(theme.themedFont(size: 12, style: .mono, weight: .bold))
.foregroundColor(.hnOrange)
.redacted(reason: .placeholder)
Text("Some Short Title")
- .font(theme.userMonoFont(size: 16, weight: .bold))
+ .font(theme.themedFont(size: 16, style: .mono, weight: .bold))
.redacted(reason: .placeholder)
HStack(spacing: 16) {
HStack(spacing: 4) {
@@ -118,7 +118,7 @@ struct StoryRowLoadingState: View {
.redacted(reason: .placeholder)
Text("99")
.font(
- theme.userSansFont(size: 12, weight: .medium)
+ theme.themedFont(size: 12, style: .sans, weight: .medium)
)
.redacted(reason: .placeholder)
}
@@ -128,7 +128,7 @@ struct StoryRowLoadingState: View {
.foregroundColor(.purple)
.redacted(reason: .placeholder)
Text("2h ago")
- .font(theme.userSansFont(size: 12, weight: .medium))
+ .font(theme.themedFont(size: 12, style: .sans, weight: .medium))
.redacted(reason: .placeholder)
}
Spacer()
@@ -138,7 +138,7 @@ struct StoryRowLoadingState: View {
Image(systemName: "message.fill")
.font(.system(size: 12))
Text("45")
- .font(theme.userSansFont(size: 12, weight: .medium))
+ .font(theme.themedFont(size: 12, style: .sans, weight: .medium))
}
.foregroundStyle(.blue)
}
diff --git a/ios/HackerNewsHomeWidget/HackerNewsHomeWidget.swift b/ios/HackerNewsHomeWidget/HackerNewsHomeWidget.swift
index 12cfa68b..95d84897 100644
--- a/ios/HackerNewsHomeWidget/HackerNewsHomeWidget.swift
+++ b/ios/HackerNewsHomeWidget/HackerNewsHomeWidget.swift
@@ -110,10 +110,10 @@ struct HackerNewsHomeWidgetEntryView: View {
HStack(spacing: 12) {
HStack(spacing: 4) {
Image(systemName: "arrow.up")
- .font(theme.userSansFont(size: 10))
+ .font(theme.themedFont(size: 10, style: .sans))
.foregroundColor(.green)
Text("\(story.score)")
- .font(theme.userSansFont(size: 10, weight: .medium))
+ .font(theme.themedFont(size: 10, style: .sans, weight: .medium))
}
HStack(spacing: 4) {
@@ -144,10 +144,10 @@ struct HackerNewsHomeWidgetEntryView: View {
HStack(spacing: 16) {
HStack(spacing: 4) {
Image(systemName: "arrow.up")
- .font(theme.userSansFont(size: 12))
+ .font(theme.themedFont(size: 12, style: .sans))
.foregroundColor(.green)
Text("\(story.score)")
- .font(theme.userSansFont(size: 12, weight: .medium))
+ .font(theme.themedFont(size: 12, style: .sans, weight: .medium))
}
HStack(spacing: 4) {
@@ -190,10 +190,10 @@ struct HackerNewsHomeWidgetEntryView: View {
HStack(spacing: 16) {
HStack(spacing: 4) {
Image(systemName: "arrow.up")
- .font(theme.userSansFont(size: 12))
+ .font(theme.themedFont(size: 12, style: .sans))
.foregroundColor(.green)
Text("\(story.score)")
- .font(theme.userSansFont(size: 12, weight: .medium))
+ .font(theme.themedFont(size: 12, style: .sans, weight: .medium))
}
HStack(spacing: 4) {
diff --git a/ios/Packages/Common/Sources/Common/Utils/Theme.swift b/ios/Packages/Common/Sources/Common/Utils/Theme.swift
index be6fb76f..7cbe0dcf 100644
--- a/ios/Packages/Common/Sources/Common/Utils/Theme.swift
+++ b/ios/Packages/Common/Sources/Common/Utils/Theme.swift
@@ -6,13 +6,112 @@ public enum ThemeContext {
case widget
}
+public enum FontStyle {
+ case sans
+ case mono
+}
+
+public enum FontStylePreference: String {
+ case sans
+ case sansAndMono
+
+ public var displayName: String {
+ switch self {
+ case .sans:
+ "Sans"
+ case .sansAndMono:
+ "Sans + Mono"
+ }
+ }
+}
+
+public enum FontFamilyPreference: String {
+ case system
+ case ibmPlex
+
+ public var displayName: String {
+ switch self {
+ case .system:
+ "System"
+ case .ibmPlex:
+ "IBM Plex"
+ }
+ }
+
+ func font(size: CGFloat, style: FontStyle, weight: Font.Weight) -> Font {
+ switch self {
+ case .ibmPlex:
+ switch style {
+ case .sans:
+ switch weight {
+ case .regular:
+ return .ibmPlexSans(.regular, size: size)
+ case .bold:
+ return .ibmPlexSans(.bold, size: size)
+ case .medium:
+ return .ibmPlexSans(.medium, size: size)
+ default:
+ return .ibmPlexSans(.regular, size: size)
+ }
+ case .mono:
+ switch weight {
+ case .regular:
+ return .ibmPlexMono(.regular, size: size)
+ case .bold:
+ return .ibmPlexMono(.bold, size: size)
+ case .medium:
+ return .ibmPlexMono(.medium, size: size)
+ default:
+ return .ibmPlexMono(.regular, size: size)
+ }
+ }
+ case .system:
+ let design: Font.Design = (style == .mono) ? .monospaced : .default
+ return .system(size: size, weight: weight, design: design)
+ }
+ }
+}
+
@MainActor
@Observable
public final class Theme {
+
private static let useSystemFontKey = "useSystemFont"
private static let useMonospacedKey = "useMonospaced"
private static let commentFontSizeKey = "commentFontSize"
private static let titleFontSizeKey = "titleFontSize"
+ private static let fontFamilyKey = "fontFamily"
+ private static let fontStyleKey = "fontStyle"
+
+ /// Converts the legacy `useSystemFont` / `useMonospaced` keys into the new
+ /// `fontFamilyPreference` / `fontStylePreference` keys the first time the
+ /// app runs with this build.
+ private static func migrateLegacyPreferences() {
+ let defaults = UserDefaults.standard
+
+ // ---- Font family ----
+ // Only migrate if the legacy key actually exists *and* the new key is absent.
+ if defaults.string(forKey: fontFamilyKey) == nil,
+ let legacyUseSystem = defaults.object(forKey: useSystemFontKey) as? Bool
+ {
+
+ let family: FontFamilyPreference = legacyUseSystem ? .system : .ibmPlex
+ defaults.set(family.rawValue, forKey: fontFamilyKey)
+ // Remove the migrated legacy key so the check will not run again.
+ defaults.removeObject(forKey: useSystemFontKey)
+ }
+
+ // ---- Font style ----
+ // Only migrate if the legacy key actually exists *and* the new key is absent.
+ if defaults.string(forKey: fontStyleKey) == nil,
+ let legacyUseMono = defaults.object(forKey: useMonospacedKey) as? Bool
+ {
+
+ let style: FontStylePreference = legacyUseMono ? .sansAndMono : .sans
+ defaults.set(style.rawValue, forKey: fontStyleKey)
+ defaults.removeObject(forKey: useMonospacedKey)
+ }
+ }
public static let defaultCommentFontSize: Double = 12
public static let minCommentFontSize: Double = 10
@@ -25,27 +124,34 @@ public final class Theme {
private let context: ThemeContext
- public var useSystemFont: Bool {
+ public var fontFamilyPreference: FontFamilyPreference {
didSet {
- UserDefaults.standard.set(useSystemFont, forKey: Self.useSystemFontKey)
+ UserDefaults.standard.set(
+ fontFamilyPreference.rawValue,
+ forKey: Self.fontFamilyKey
+ )
}
}
- public var useMonospaced: Bool {
+ public var fontStylePreference: FontStylePreference {
didSet {
- UserDefaults.standard.set(useMonospaced, forKey: Self.useMonospacedKey)
+ UserDefaults.standard
+ .set(fontStylePreference.rawValue, forKey: Self.fontStyleKey)
}
}
public var commentFontSize: Double {
didSet {
let clamped = commentFontSize.clamped(
- to: Self.minCommentFontSize...Self.maxCommentFontSize)
+ to: Self.minCommentFontSize...Self.maxCommentFontSize
+ )
if clamped != commentFontSize {
commentFontSize = clamped
}
UserDefaults.standard.set(
- commentFontSize, forKey: Self.commentFontSizeKey)
+ commentFontSize,
+ forKey: Self.commentFontSizeKey
+ )
}
}
@@ -56,7 +162,8 @@ public final class Theme {
public var titleFontSize: Double {
didSet {
let clamped = titleFontSize.clamped(
- to: Self.minTitleFontSize...Self.maxTitleFontSize)
+ to: Self.minTitleFontSize...Self.maxTitleFontSize
+ )
if clamped != titleFontSize {
titleFontSize = clamped
}
@@ -66,64 +173,48 @@ public final class Theme {
public var titleFont: Font {
let size = context == .app ? titleFontSize : Self.defaultWidgetTitleFontSize
- return userMonoFont(size: size, weight: .bold)
+ return themedFont(size: size, style: .mono, weight: .bold)
}
public var commentTextFont: Font {
- userMonoFont(size: commentFontSize, weight: .regular)
+ themedFont(size: commentFontSize, style: .mono, weight: .regular)
}
public var commentAuthorFont: Font {
- userMonoFont(size: commentFontSize, weight: .bold)
+ themedFont(size: commentFontSize, style: .mono, weight: .bold)
}
public var commentMetadataFont: Font {
- userSansFont(size: commentFontSize, weight: .medium)
+ themedFont(size: commentFontSize, style: .sans, weight: .medium)
}
- public func userMonoFont(size: CGFloat, weight: Font.Weight = .regular) -> Font {
- if !useMonospaced {
- return userSansFont(size: size, weight: weight)
- }
- if useSystemFont {
- return .system(size: size, weight: weight, design: .default)
- }
- switch weight {
- case .regular:
- return .ibmPlexMono(.regular, size: size)
- case .bold:
- return .ibmPlexMono(.bold, size: size)
- case .medium:
- return .ibmPlexMono(.medium, size: size)
- default:
- return .ibmPlexMono(.regular, size: size)
- }
- }
-
- public func userSansFont(size: CGFloat, weight: Font.Weight = .regular) -> Font {
- if useSystemFont {
- return .system(size: size, weight: weight, design: .default)
- }
- switch weight {
- case .regular:
- return .ibmPlexSans(.regular, size: size)
- case .bold:
- return .ibmPlexSans(.bold, size: size)
- case .medium:
- return .ibmPlexSans(.medium, size: size)
- default:
- return .ibmPlexSans(.regular, size: size)
+ public func themedFont(
+ size: CGFloat,
+ style: FontStyle,
+ weight: Font.Weight = .regular
+ ) -> Font {
+ var style = style
+ if fontStylePreference == .sans {
+ style = .sans
}
+ return fontFamilyPreference.font(size: size, style: style, weight: weight)
}
public init(context: ThemeContext = .app) {
+ Self.migrateLegacyPreferences()
self.context = context
- self.useSystemFont =
- UserDefaults.standard.object(forKey: Self.useSystemFontKey) as? Bool
- ?? false
- self.useMonospaced =
- UserDefaults.standard.object(forKey: Self.useMonospacedKey) as? Bool
- ?? true
+ let fontStylePrefValue = UserDefaults.standard.string(
+ forKey: Self.fontStyleKey
+ )
+ self.fontStylePreference =
+ fontStylePrefValue
+ .flatMap { FontStylePreference(rawValue: $0) } ?? .sansAndMono
+ let fontFamilyPrefValue = UserDefaults.standard.string(
+ forKey: Self.fontFamilyKey
+ )
+ self.fontFamilyPreference =
+ fontFamilyPrefValue
+ .flatMap { FontFamilyPreference(rawValue: $0) } ?? .ibmPlex
self.commentFontSize =
UserDefaults.standard.object(forKey: Self.commentFontSizeKey) as? Double
?? Self.defaultCommentFontSize
diff --git a/ios/Packages/Fonts/Package.swift b/ios/Packages/Fonts/Package.swift
index ddd22a23..be3b274e 100644
--- a/ios/Packages/Fonts/Package.swift
+++ b/ios/Packages/Fonts/Package.swift
@@ -19,9 +19,12 @@ let package = Package(
.target(
name: "Fonts",
resources: [
- .copy("ibm_plex_sans_bold.ttf"),
- .copy("ibm_plex_sans_medium.ttf"),
- .copy("ibm_plex_sans_regular.ttf")
+ .copy("IBMPlexSans-Bold.ttf"),
+ .copy("IBMPlexSans-Medium.ttf"),
+ .copy("IBMPlexSans-Regular.ttf"),
+ .copy("IBMPlexMono-Bold.ttf"),
+ .copy("IBMPlexMono-Medium.ttf"),
+ .copy("IBMPlexMono-Regular.ttf")
],
swiftSettings: [
.swiftLanguageMode(.v5)
diff --git a/ios/Packages/Fonts/Sources/Fonts/ibm_plex_sans_bold.ttf b/ios/Packages/Fonts/Sources/Fonts/IBMPlexMono-Bold.ttf
similarity index 100%
rename from ios/Packages/Fonts/Sources/Fonts/ibm_plex_sans_bold.ttf
rename to ios/Packages/Fonts/Sources/Fonts/IBMPlexMono-Bold.ttf
diff --git a/ios/Packages/Fonts/Sources/Fonts/IBMPlexMono-Medium.ttf b/ios/Packages/Fonts/Sources/Fonts/IBMPlexMono-Medium.ttf
new file mode 100644
index 00000000..39f178db
Binary files /dev/null and b/ios/Packages/Fonts/Sources/Fonts/IBMPlexMono-Medium.ttf differ
diff --git a/ios/Packages/Fonts/Sources/Fonts/ibm_plex_sans_regular.ttf b/ios/Packages/Fonts/Sources/Fonts/IBMPlexMono-Regular.ttf
similarity index 100%
rename from ios/Packages/Fonts/Sources/Fonts/ibm_plex_sans_regular.ttf
rename to ios/Packages/Fonts/Sources/Fonts/IBMPlexMono-Regular.ttf
diff --git a/ios/Packages/Fonts/Sources/Fonts/IBMPlexSans-Bold.ttf b/ios/Packages/Fonts/Sources/Fonts/IBMPlexSans-Bold.ttf
new file mode 100644
index 00000000..258c10a9
Binary files /dev/null and b/ios/Packages/Fonts/Sources/Fonts/IBMPlexSans-Bold.ttf differ
diff --git a/ios/Packages/Fonts/Sources/Fonts/IBMPlexSans-Medium.ttf b/ios/Packages/Fonts/Sources/Fonts/IBMPlexSans-Medium.ttf
new file mode 100644
index 00000000..fb75072d
Binary files /dev/null and b/ios/Packages/Fonts/Sources/Fonts/IBMPlexSans-Medium.ttf differ
diff --git a/ios/Packages/Fonts/Sources/Fonts/IBMPlexSans-Regular.ttf b/ios/Packages/Fonts/Sources/Fonts/IBMPlexSans-Regular.ttf
new file mode 100644
index 00000000..5387ad48
Binary files /dev/null and b/ios/Packages/Fonts/Sources/Fonts/IBMPlexSans-Regular.ttf differ
diff --git a/ios/Packages/Fonts/Sources/Fonts/ibm_plex_sans_medium.ttf b/ios/Packages/Fonts/Sources/Fonts/ibm_plex_sans_medium.ttf
deleted file mode 100644
index 9395402b..00000000
Binary files a/ios/Packages/Fonts/Sources/Fonts/ibm_plex_sans_medium.ttf and /dev/null differ