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 = {
CODE_SIGN_ENTITLEMENTS = URLShare/URLShare.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 33;
CURRENT_PROJECT_VERSION = 34;
DEVELOPMENT_TEAM = 8J69P655GN;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = URLShare/Info.plist;
@ -485,7 +485,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = URLShare/URLShare.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 33;
CURRENT_PROJECT_VERSION = 34;
DEVELOPMENT_TEAM = 8J69P655GN;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = URLShare/Info.plist;
@ -640,7 +640,7 @@
CODE_SIGN_ENTITLEMENTS = readeck/readeck.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 33;
CURRENT_PROJECT_VERSION = 34;
DEVELOPMENT_ASSET_PATHS = "\"readeck/Preview Content\"";
DEVELOPMENT_TEAM = 8J69P655GN;
ENABLE_HARDENED_RUNTIME = YES;
@ -684,7 +684,7 @@
CODE_SIGN_ENTITLEMENTS = readeck/readeck.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 33;
CURRENT_PROJECT_VERSION = 34;
DEVELOPMENT_ASSET_PATHS = "\"readeck/Preview Content\"";
DEVELOPMENT_TEAM = 8J69P655GN;
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
- 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
- Better color consistency throughout the app

View File

@ -28,48 +28,17 @@ struct AppearanceSettingsView: View {
var body: some View {
Group {
Section {
// Font Family
Picker("Font family", selection: $fontViewModel.selectedFontFamily) {
ForEach(FontFamily.allCases, id: \.self) { family in
Text(family.displayName).tag(family)
}
}
.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)
// Font Settings als NavigationLink
NavigationLink {
FontSelectionView(viewModel: fontViewModel)
} label: {
HStack {
Text("Font")
Spacer()
Text("\(fontViewModel.selectedFontFamily.displayName) · \(fontViewModel.selectedFontSize.displayName)")
.foregroundColor(.secondary)
}
.padding(.vertical, 4)
.listRowBackground(Color(.systemGray6))
}
// Theme Picker (Menu statt Segmented)
Picker("Theme", selection: $selectedTheme) {
@ -97,6 +66,7 @@ struct AppearanceSettingsView: View {
}
// Open external links in
VStack(alignment: .leading, spacing: 4) {
Picker("Open links in", selection: $generalViewModel.urlOpener) {
ForEach(UrlOpener.allCases, id: \.self) { urlOpener in
Text(urlOpener.displayName).tag(urlOpener)
@ -108,7 +78,14 @@ struct AppearanceSettingsView: View {
}
}
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
VStack(alignment: .leading, spacing: 4) {
Picker("Tag sort order", selection: $selectedTagSortOrder) {
ForEach(TagSortOrder.allCases, id: \.self) { sortOrder in
Text(sortOrder.displayName).tag(sortOrder)
@ -117,10 +94,14 @@ struct AppearanceSettingsView: View {
.onChange(of: selectedTagSortOrder) {
saveTagSortOrderSettings()
}
Text("Determines how tags are displayed when adding or editing bookmarks.")
.font(.caption)
.foregroundColor(.secondary)
.padding(.top, 2)
}
} header: {
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 {

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 {
@State private var showingPrivacyPolicy = false
@State private var showingLegalNotice = false
@State private var showReleaseNotes = false
var body: some View {
Group {
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: {
showingPrivacyPolicy = true
}) {
@ -30,11 +46,7 @@ struct LegalPrivacySettingsView: View {
.foregroundColor(.secondary)
}
}
} header: {
Text("Legal & Privacy")
}
Section {
Button(action: {
if let url = URL(string: "https://github.com/ilyas-hallak/readeck-ios/issues") {
UIApplication.shared.open(url)
@ -63,7 +75,7 @@ struct LegalPrivacySettingsView: View {
}
}
} header: {
Text("Support")
Text("Legal, Privacy & Support")
}
}
.sheet(isPresented: $showingPrivacyPolicy) {
@ -72,6 +84,9 @@ struct LegalPrivacySettingsView: View {
.sheet(isPresented: $showingLegalNotice) {
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 {
AppearanceSettingsView()
ReadingSettingsView()
CacheSettingsView()
SettingsGeneralView()
SyncSettingsView()
SettingsServerView()
@ -80,39 +82,42 @@ struct SettingsContainerView: View {
@ViewBuilder
private var appInfoSection: some View {
Section {
VStack(spacing: 8) {
HStack(spacing: 8) {
VStack(alignment: .leading, spacing: 6) {
HStack(spacing: 6) {
Image(systemName: "info.circle")
.font(.caption)
.foregroundColor(.secondary)
Text("Version \(appVersion)")
.font(.footnote)
.font(.caption)
.foregroundColor(.secondary)
}
HStack(spacing: 4) {
Image(systemName: "person.crop.circle")
.font(.caption)
.foregroundColor(.secondary)
Text("Developer:")
.font(.footnote)
.font(.caption)
.foregroundColor(.secondary)
Button("Ilyas Hallak") {
if let url = URL(string: "https://ilyashallak.de") {
UIApplication.shared.open(url)
}
}
.font(.footnote)
.foregroundColor(.blue)
.font(.caption)
}
HStack(spacing: 8) {
HStack(spacing: 6) {
Image(systemName: "globe")
.font(.caption)
.foregroundColor(.secondary)
Text("From Bremen with 💚")
.font(.footnote)
.font(.caption)
.foregroundColor(.secondary)
}
}
.frame(maxWidth: .infinity)
.frame(maxWidth: .infinity, alignment: .center)
.listRowBackground(Color.clear)
.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)
}