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