Redesign settings screen with native iOS style

- Move font settings to dedicated detail screen with larger preview
- Add inline explanations directly under each setting
- Reorganize sections: split General into Reading Settings and Sync Settings
- Combine Legal, Privacy and Support into single section
- Move "What's New" to combined Legal/Privacy/Support section
- Redesign app info footer with muted styling and center alignment
- Remove white backgrounds from font preview for better light/dark mode support
This commit is contained in:
Ilyas Hallak 2025-11-08 19:12:08 +01:00
parent f3719fa9d4
commit 4b788650b8
8 changed files with 308 additions and 74 deletions

View File

@ -452,7 +452,7 @@
buildSettings = { buildSettings = {
CODE_SIGN_ENTITLEMENTS = URLShare/URLShare.entitlements; CODE_SIGN_ENTITLEMENTS = URLShare/URLShare.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 33; CURRENT_PROJECT_VERSION = 34;
DEVELOPMENT_TEAM = 8J69P655GN; DEVELOPMENT_TEAM = 8J69P655GN;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = URLShare/Info.plist; INFOPLIST_FILE = URLShare/Info.plist;
@ -485,7 +485,7 @@
buildSettings = { buildSettings = {
CODE_SIGN_ENTITLEMENTS = URLShare/URLShare.entitlements; CODE_SIGN_ENTITLEMENTS = URLShare/URLShare.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 33; CURRENT_PROJECT_VERSION = 34;
DEVELOPMENT_TEAM = 8J69P655GN; DEVELOPMENT_TEAM = 8J69P655GN;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = URLShare/Info.plist; INFOPLIST_FILE = URLShare/Info.plist;
@ -640,7 +640,7 @@
CODE_SIGN_ENTITLEMENTS = readeck/readeck.entitlements; CODE_SIGN_ENTITLEMENTS = readeck/readeck.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 33; CURRENT_PROJECT_VERSION = 34;
DEVELOPMENT_ASSET_PATHS = "\"readeck/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"readeck/Preview Content\"";
DEVELOPMENT_TEAM = 8J69P655GN; DEVELOPMENT_TEAM = 8J69P655GN;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
@ -684,7 +684,7 @@
CODE_SIGN_ENTITLEMENTS = readeck/readeck.entitlements; CODE_SIGN_ENTITLEMENTS = readeck/readeck.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 33; CURRENT_PROJECT_VERSION = 34;
DEVELOPMENT_ASSET_PATHS = "\"readeck/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"readeck/Preview Content\"";
DEVELOPMENT_TEAM = 8J69P655GN; DEVELOPMENT_TEAM = 8J69P655GN;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;

View File

@ -23,6 +23,15 @@ Thanks for using the Readeck iOS app! Below are the release notes for each versi
- Better performance when working with many labels - Better performance when working with many labels
- Improved overall app stability - Improved overall app stability
### Settings Redesign
- **Completely redesigned settings screen** with native iOS style
- Font settings moved to dedicated screen with larger preview
- Reorganized sections for better overview
- Inline explanations directly under settings
- Cleaner app info footer with muted styling
- Combined legal, privacy and support into one section
### Fixes & Improvements ### Fixes & Improvements
- Better color consistency throughout the app - Better color consistency throughout the app

View File

@ -28,48 +28,17 @@ struct AppearanceSettingsView: View {
var body: some View { var body: some View {
Group { Group {
Section { Section {
// Font Family // Font Settings als NavigationLink
Picker("Font family", selection: $fontViewModel.selectedFontFamily) { NavigationLink {
ForEach(FontFamily.allCases, id: \.self) { family in FontSelectionView(viewModel: fontViewModel)
Text(family.displayName).tag(family) } label: {
HStack {
Text("Font")
Spacer()
Text("\(fontViewModel.selectedFontFamily.displayName) · \(fontViewModel.selectedFontSize.displayName)")
.foregroundColor(.secondary)
} }
} }
.onChange(of: fontViewModel.selectedFontFamily) {
Task {
await fontViewModel.saveFontSettings()
}
}
// Font Size
Picker("Font size", selection: $fontViewModel.selectedFontSize) {
ForEach(FontSize.allCases, id: \.self) { size in
Text(size.displayName).tag(size)
}
}
.pickerStyle(.segmented)
.onChange(of: fontViewModel.selectedFontSize) {
Task {
await fontViewModel.saveFontSettings()
}
}
// Font Preview - direkt in der gleichen Section
VStack(alignment: .leading, spacing: 6) {
Text("readeck Bookmark Title")
.font(fontViewModel.previewTitleFont)
.fontWeight(.semibold)
.lineLimit(1)
Text("This is how your bookmark descriptions and article text will appear in the app. The quick brown fox jumps over the lazy dog.")
.font(fontViewModel.previewBodyFont)
.lineLimit(3)
Text("12 min • Today • example.com")
.font(fontViewModel.previewCaptionFont)
.foregroundColor(.secondary)
}
.padding(.vertical, 4)
.listRowBackground(Color(.systemGray6))
// Theme Picker (Menu statt Segmented) // Theme Picker (Menu statt Segmented)
Picker("Theme", selection: $selectedTheme) { Picker("Theme", selection: $selectedTheme) {
@ -97,30 +66,42 @@ struct AppearanceSettingsView: View {
} }
// Open external links in // Open external links in
Picker("Open links in", selection: $generalViewModel.urlOpener) { VStack(alignment: .leading, spacing: 4) {
ForEach(UrlOpener.allCases, id: \.self) { urlOpener in Picker("Open links in", selection: $generalViewModel.urlOpener) {
Text(urlOpener.displayName).tag(urlOpener) ForEach(UrlOpener.allCases, id: \.self) { urlOpener in
Text(urlOpener.displayName).tag(urlOpener)
}
} }
} .onChange(of: generalViewModel.urlOpener) {
.onChange(of: generalViewModel.urlOpener) { Task {
Task { await generalViewModel.saveGeneralSettings()
await generalViewModel.saveGeneralSettings() }
} }
Text("Choose where external links should open: In-App Browser keeps you in readeck, Default Browser opens in Safari or your default browser.")
.font(.caption)
.foregroundColor(.secondary)
.padding(.top, 2)
} }
// Tag Sort Order // Tag Sort Order
Picker("Tag sort order", selection: $selectedTagSortOrder) { VStack(alignment: .leading, spacing: 4) {
ForEach(TagSortOrder.allCases, id: \.self) { sortOrder in Picker("Tag sort order", selection: $selectedTagSortOrder) {
Text(sortOrder.displayName).tag(sortOrder) ForEach(TagSortOrder.allCases, id: \.self) { sortOrder in
Text(sortOrder.displayName).tag(sortOrder)
}
} }
} .onChange(of: selectedTagSortOrder) {
.onChange(of: selectedTagSortOrder) { saveTagSortOrderSettings()
saveTagSortOrderSettings() }
Text("Determines how tags are displayed when adding or editing bookmarks.")
.font(.caption)
.foregroundColor(.secondary)
.padding(.top, 2)
} }
} header: { } header: {
Text("Appearance") Text("Appearance")
} footer: {
Text("Choose where external links should open: In-App Browser keeps you in readeck, Default Browser opens in Safari or your default browser.\n\nTag sort order determines how tags are displayed when adding or editing bookmarks.")
} }
} }
.task { .task {

View File

@ -0,0 +1,105 @@
//
// FontSelectionView.swift
// readeck
//
// Created by Ilyas Hallak on 08.11.25.
//
import SwiftUI
struct FontSelectionView: View {
@State private var viewModel: FontSettingsViewModel
@Environment(\.dismiss) private var dismiss
init(viewModel: FontSettingsViewModel = FontSettingsViewModel()) {
self.viewModel = viewModel
}
var body: some View {
List {
// Preview Section
Section {
VStack(alignment: .leading, spacing: 12) {
Text("readeck Bookmark Title")
.font(viewModel.previewTitleFont)
.fontWeight(.semibold)
.lineLimit(2)
Text("This is how your bookmark descriptions and article text will appear in the app. The quick brown fox jumps over the lazy dog. Lorem ipsum dolor sit amet, consectetur adipiscing elit.")
.font(viewModel.previewBodyFont)
.lineLimit(4)
Text("12 min • Today • example.com")
.font(viewModel.previewCaptionFont)
.foregroundColor(.secondary)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(16)
.background(Color(.systemBackground))
.clipShape(RoundedRectangle(cornerRadius: 12))
.shadow(color: Color.black.opacity(0.08), radius: 8, x: 0, y: 2)
.listRowInsets(EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16))
.listRowBackground(Color.clear)
} header: {
Text("Preview")
.font(.subheadline)
.fontWeight(.semibold)
.foregroundColor(.primary)
.textCase(nil)
}
// Font Settings Section
Section {
Picker("Font family", selection: $viewModel.selectedFontFamily) {
ForEach(FontFamily.allCases, id: \.self) { family in
Text(family.displayName).tag(family)
}
}
.onChange(of: viewModel.selectedFontFamily) {
Task {
await viewModel.saveFontSettings()
}
}
VStack(alignment: .leading, spacing: 8) {
Text("Font size")
.font(.subheadline)
.foregroundColor(.primary)
Picker("Font size", selection: $viewModel.selectedFontSize) {
ForEach(FontSize.allCases, id: \.self) { size in
Text(size.displayName).tag(size)
}
}
.pickerStyle(.segmented)
.onChange(of: viewModel.selectedFontSize) {
Task {
await viewModel.saveFontSettings()
}
}
}
.padding(.vertical, 4)
} header: {
Text("Font Settings")
.font(.subheadline)
.fontWeight(.semibold)
.foregroundColor(.primary)
.textCase(nil)
}
}
.listStyle(.insetGrouped)
.navigationTitle("Font")
.navigationBarTitleDisplayMode(.inline)
.task {
await viewModel.loadFontSettings()
}
}
}
#Preview {
NavigationStack {
FontSelectionView(viewModel: .init(
factory: MockUseCaseFactory()
))
}
}

View File

@ -3,10 +3,26 @@ import SwiftUI
struct LegalPrivacySettingsView: View { struct LegalPrivacySettingsView: View {
@State private var showingPrivacyPolicy = false @State private var showingPrivacyPolicy = false
@State private var showingLegalNotice = false @State private var showingLegalNotice = false
@State private var showReleaseNotes = false
var body: some View { var body: some View {
Group { Group {
Section { Section {
Button(action: {
showReleaseNotes = true
}) {
HStack {
Text("What's New")
Spacer()
Text("Version \(VersionManager.shared.currentVersion)")
.font(.caption)
.foregroundColor(.secondary)
Image(systemName: "chevron.right")
.font(.caption)
.foregroundColor(.secondary)
}
}
Button(action: { Button(action: {
showingPrivacyPolicy = true showingPrivacyPolicy = true
}) { }) {
@ -30,11 +46,7 @@ struct LegalPrivacySettingsView: View {
.foregroundColor(.secondary) .foregroundColor(.secondary)
} }
} }
} header: {
Text("Legal & Privacy")
}
Section {
Button(action: { Button(action: {
if let url = URL(string: "https://github.com/ilyas-hallak/readeck-ios/issues") { if let url = URL(string: "https://github.com/ilyas-hallak/readeck-ios/issues") {
UIApplication.shared.open(url) UIApplication.shared.open(url)
@ -63,7 +75,7 @@ struct LegalPrivacySettingsView: View {
} }
} }
} header: { } header: {
Text("Support") Text("Legal, Privacy & Support")
} }
} }
.sheet(isPresented: $showingPrivacyPolicy) { .sheet(isPresented: $showingPrivacyPolicy) {
@ -72,6 +84,9 @@ struct LegalPrivacySettingsView: View {
.sheet(isPresented: $showingLegalNotice) { .sheet(isPresented: $showingLegalNotice) {
LegalNoticeView() LegalNoticeView()
} }
.sheet(isPresented: $showReleaseNotes) {
ReleaseNotesView()
}
} }
} }

View File

@ -0,0 +1,55 @@
//
// ReadingSettingsView.swift
// readeck
//
// Created by Ilyas Hallak on 08.11.25.
//
import SwiftUI
struct ReadingSettingsView: View {
@State private var viewModel: SettingsGeneralViewModel
init(viewModel: SettingsGeneralViewModel = SettingsGeneralViewModel()) {
self.viewModel = viewModel
}
var body: some View {
Group {
Section {
VStack(alignment: .leading, spacing: 4) {
Toggle("Read Aloud Feature", isOn: $viewModel.enableTTS)
.onChange(of: viewModel.enableTTS) {
Task {
await viewModel.saveGeneralSettings()
}
}
Text("Activate the Read Aloud Feature to read aloud your articles. This is a really early preview and might not work perfectly.")
.font(.caption)
.foregroundColor(.secondary)
.padding(.top, 2)
}
#if DEBUG
Toggle("Safari Reader Mode", isOn: $viewModel.enableReaderMode)
Toggle("Automatically mark articles as read", isOn: $viewModel.autoMarkAsRead)
#endif
} header: {
Text("Reading Settings")
}
}
.task {
await viewModel.loadGeneralSettings()
}
}
}
#Preview {
List {
ReadingSettingsView(viewModel: .init(
MockUseCaseFactory()
))
}
.listStyle(.insetGrouped)
}

View File

@ -19,9 +19,11 @@ struct SettingsContainerView: View {
List { List {
AppearanceSettingsView() AppearanceSettingsView()
ReadingSettingsView()
CacheSettingsView() CacheSettingsView()
SettingsGeneralView() SyncSettingsView()
SettingsServerView() SettingsServerView()
@ -80,39 +82,42 @@ struct SettingsContainerView: View {
@ViewBuilder @ViewBuilder
private var appInfoSection: some View { private var appInfoSection: some View {
Section { Section {
VStack(spacing: 8) { VStack(alignment: .leading, spacing: 6) {
HStack(spacing: 8) { HStack(spacing: 6) {
Image(systemName: "info.circle") Image(systemName: "info.circle")
.font(.caption)
.foregroundColor(.secondary) .foregroundColor(.secondary)
Text("Version \(appVersion)") Text("Version \(appVersion)")
.font(.footnote) .font(.caption)
.foregroundColor(.secondary) .foregroundColor(.secondary)
} }
HStack(spacing: 4) { HStack(spacing: 4) {
Image(systemName: "person.crop.circle") Image(systemName: "person.crop.circle")
.font(.caption)
.foregroundColor(.secondary) .foregroundColor(.secondary)
Text("Developer:") Text("Developer:")
.font(.footnote) .font(.caption)
.foregroundColor(.secondary) .foregroundColor(.secondary)
Button("Ilyas Hallak") { Button("Ilyas Hallak") {
if let url = URL(string: "https://ilyashallak.de") { if let url = URL(string: "https://ilyashallak.de") {
UIApplication.shared.open(url) UIApplication.shared.open(url)
} }
} }
.font(.footnote) .font(.caption)
.foregroundColor(.blue)
} }
HStack(spacing: 8) { HStack(spacing: 6) {
Image(systemName: "globe") Image(systemName: "globe")
.font(.caption)
.foregroundColor(.secondary) .foregroundColor(.secondary)
Text("From Bremen with 💚") Text("From Bremen with 💚")
.font(.footnote) .font(.caption)
.foregroundColor(.secondary) .foregroundColor(.secondary)
} }
} }
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity, alignment: .center)
.listRowBackground(Color.clear)
.padding(.vertical, 8) .padding(.vertical, 8)
} }
} }

View File

@ -0,0 +1,64 @@
//
// SyncSettingsView.swift
// readeck
//
// Created by Ilyas Hallak on 08.11.25.
//
import SwiftUI
struct SyncSettingsView: View {
@State private var viewModel: SettingsGeneralViewModel
init(viewModel: SettingsGeneralViewModel = SettingsGeneralViewModel()) {
self.viewModel = viewModel
}
var body: some View {
Group {
#if DEBUG
Section {
Toggle("Automatic sync", isOn: $viewModel.autoSyncEnabled)
if viewModel.autoSyncEnabled {
Stepper("Sync interval: \(viewModel.syncInterval) minutes", value: $viewModel.syncInterval, in: 1...60)
}
} header: {
Text("Sync Settings")
}
if let successMessage = viewModel.successMessage {
Section {
HStack {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
Text(successMessage)
.foregroundColor(.green)
}
}
}
if let errorMessage = viewModel.errorMessage {
Section {
HStack {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundColor(.red)
Text(errorMessage)
.foregroundColor(.red)
}
}
}
#endif
}
.task {
await viewModel.loadGeneralSettings()
}
}
}
#Preview {
List {
SyncSettingsView(viewModel: .init(
MockUseCaseFactory()
))
}
.listStyle(.insetGrouped)
}