IceCubes/IceCubesApp/App/Tabs/Settings/DisplaySettingsView.swift
Paul Schuetz 59b16d86a7
Fix the lag of the display setting sliders (#1342) close #1341
* Fix the lag of the display setting sliders

The sliders in the display settings were laggy because changing the value in the
theme class needs comparatively much time. This writing to the class is now
only done when the user lets go of the slider, so sliding it is responsive. I
was unable to make writing to the class more responsive, the example post needs
therefore a short time before it accepts the new values. Furthermore, all
AppStorage keys are now realized with a ThemeKey to clean the class up.
Fixes #1341

Signed-off-by: Paul Schuetz <pa.schuetz@web.de>

* Remove unnecessary changes to theme

The changing of theme keys is unnecessary for the fix oft the lag, so it's
removed.

Signed-off-by: Paul Schuetz <pa.schuetz@web.de>

* Rework fix to be more similar to other lag fixes

The fix now uses the ...LocalValues class, which published updates only every
half a second.

Signed-off-by: Paul Schuetz <pa.schuetz@web.de>

---------

Signed-off-by: Paul Schuetz <pa.schuetz@web.de>
2023-03-31 15:34:24 +02:00

257 lines
9.5 KiB
Swift

import Combine
import DesignSystem
import Env
import Models
import Network
import Status
import SwiftUI
class DisplaySettingsLocalValues: ObservableObject {
@Published var tintColor = Theme.shared.tintColor
@Published var primaryBackgroundColor = Theme.shared.primaryBackgroundColor
@Published var secondaryBackgroundColor = Theme.shared.secondaryBackgroundColor
@Published var labelColor = Theme.shared.labelColor
@Published var lineSpacing = Theme.shared.lineSpacing
@Published var fontSizeScale = Theme.shared.fontSizeScale
private var subscriptions = Set<AnyCancellable>()
init() {
$tintColor
.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
.sink(receiveValue: { newColor in Theme.shared.tintColor = newColor })
.store(in: &subscriptions)
$primaryBackgroundColor
.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
.sink(receiveValue: { newColor in Theme.shared.primaryBackgroundColor = newColor })
.store(in: &subscriptions)
$secondaryBackgroundColor
.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
.sink(receiveValue: { newColor in Theme.shared.secondaryBackgroundColor = newColor })
.store(in: &subscriptions)
$labelColor
.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
.sink(receiveValue: { newColor in Theme.shared.labelColor = newColor })
.store(in: &subscriptions)
$lineSpacing
.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
.sink(receiveValue: { newSpacing in Theme.shared.lineSpacing = newSpacing })
.store(in: &subscriptions)
$fontSizeScale
.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
.sink(receiveValue: { newScale in Theme.shared.fontSizeScale = newScale })
.store(in: &subscriptions)
}
}
struct DisplaySettingsView: View {
typealias FontState = Theme.FontState
@Environment(\.colorScheme) private var colorScheme
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var userPreferences: UserPreferences
@StateObject private var localValues = DisplaySettingsLocalValues()
@State private var isFontSelectorPresented = false
private let previewStatusViewModel = StatusRowViewModel(status: Status.placeholder(forSettings: true, language: "la"),
client: Client(server: ""),
routerPath: RouterPath()) // translate from latin button
var body: some View {
ZStack(alignment: .top) {
Form {
StatusRowView(viewModel: { previewStatusViewModel })
.allowsHitTesting(false)
.opacity(0)
.hidden()
themeSection
fontSection
layoutSection
platformsSection
resetSection
}
.navigationTitle("settings.display.navigation-title")
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
examplePost
}
}
private var examplePost: some View {
VStack(spacing: 0) {
StatusRowView(viewModel: { previewStatusViewModel })
.allowsHitTesting(false)
.padding(.layoutPadding)
.background(theme.primaryBackgroundColor)
.cornerRadius(8)
.padding(.horizontal, .layoutPadding)
.padding(.top, .layoutPadding)
.background(theme.secondaryBackgroundColor)
Rectangle()
.fill(theme.secondaryBackgroundColor)
.frame(height: 30)
.mask(LinearGradient(gradient: Gradient(colors: [theme.secondaryBackgroundColor, .clear]),
startPoint: .top, endPoint: .bottom))
}
}
private var themeSection: some View {
Section {
Toggle("settings.display.theme.systemColor", isOn: $theme.followSystemColorScheme)
themeSelectorButton
Group {
ColorPicker("settings.display.theme.tint", selection: $localValues.tintColor)
ColorPicker("settings.display.theme.background", selection: $localValues.primaryBackgroundColor)
ColorPicker("settings.display.theme.secondary-background", selection: $localValues.secondaryBackgroundColor)
ColorPicker("settings.display.theme.text-color", selection: $localValues.labelColor)
}
.disabled(theme.followSystemColorScheme)
.opacity(theme.followSystemColorScheme ? 0.5 : 1.0)
.onChange(of: theme.selectedSet) { _ in
localValues.tintColor = theme.tintColor
localValues.primaryBackgroundColor = theme.primaryBackgroundColor
localValues.secondaryBackgroundColor = theme.secondaryBackgroundColor
localValues.labelColor = theme.labelColor
}
} header: {
Text("settings.display.section.theme")
} footer: {
if theme.followSystemColorScheme {
Text("settings.display.section.theme.footer")
}
}
.listRowBackground(theme.primaryBackgroundColor)
}
private var fontSection: some View {
Section("settings.display.section.font") {
Picker("settings.display.font", selection: .init(get: { () -> FontState in
if theme.chosenFont?.fontName == "OpenDyslexic-Regular" {
return FontState.openDyslexic
} else if theme.chosenFont?.fontName == "AtkinsonHyperlegible-Regular" {
return FontState.hyperLegible
} else if theme.chosenFont?.fontName == ".AppleSystemUIFontRounded-Regular" {
return FontState.SFRounded
}
return theme.chosenFontData != nil ? FontState.custom : FontState.system
}, set: { newValue in
switch newValue {
case .system:
theme.chosenFont = nil
case .openDyslexic:
theme.chosenFont = UIFont(name: "OpenDyslexic", size: 1)
case .hyperLegible:
theme.chosenFont = UIFont(name: "Atkinson Hyperlegible", size: 1)
case .SFRounded:
theme.chosenFont = UIFont.systemFont(ofSize: 1).rounded()
case .custom:
isFontSelectorPresented = true
}
})) {
ForEach(FontState.allCases, id: \.rawValue) { fontState in
Text(fontState.title).tag(fontState)
}
}
.navigationDestination(isPresented: $isFontSelectorPresented, destination: { FontPicker() })
VStack {
Slider(value: $localValues.fontSizeScale, in: 0.5 ... 1.5, step: 0.1)
Text("settings.display.font.scaling-\(String(format: "%.1f", localValues.fontSizeScale))")
.font(.scaledBody)
}
.alignmentGuide(.listRowSeparatorLeading) { d in
d[.leading]
}
VStack {
Slider(value: $localValues.lineSpacing, in: 0.4 ... 10.0, step: 0.2)
Text("settings.display.font.line-spacing-\(String(format: "%.1f", localValues.lineSpacing))")
.font(.scaledBody)
}
.alignmentGuide(.listRowSeparatorLeading) { d in
d[.leading]
}
}
.listRowBackground(theme.primaryBackgroundColor)
}
private var layoutSection: some View {
Section("settings.display.section.display") {
Picker("settings.display.avatar.position", selection: $theme.avatarPosition) {
ForEach(Theme.AvatarPosition.allCases, id: \.rawValue) { position in
Text(position.description).tag(position)
}
}
Picker("settings.display.avatar.shape", selection: $theme.avatarShape) {
ForEach(Theme.AvatarShape.allCases, id: \.rawValue) { shape in
Text(shape.description).tag(shape)
}
}
Toggle("settings.display.full-username", isOn: $theme.displayFullUsername)
Picker("settings.display.status.action-buttons", selection: $theme.statusActionsDisplay) {
ForEach(Theme.StatusActionsDisplay.allCases, id: \.rawValue) { buttonStyle in
Text(buttonStyle.description).tag(buttonStyle)
}
}
Picker("settings.display.status.media-style", selection: $theme.statusDisplayStyle) {
ForEach(Theme.StatusDisplayStyle.allCases, id: \.rawValue) { buttonStyle in
Text(buttonStyle.description).tag(buttonStyle)
}
}
Toggle("settings.display.translate-button", isOn: $userPreferences.showTranslateButton)
}
.listRowBackground(theme.primaryBackgroundColor)
}
@ViewBuilder
private var platformsSection: some View {
if UIDevice.current.userInterfaceIdiom == .phone {
Section("iPhone") {
Toggle("settings.display.show-tab-label", isOn: $userPreferences.showiPhoneTabLabel)
}
.listRowBackground(theme.primaryBackgroundColor)
}
if UIDevice.current.userInterfaceIdiom == .pad {
Section("iPad") {
Toggle("settings.display.show-ipad-column", isOn: $userPreferences.showiPadSecondaryColumn)
}
.listRowBackground(theme.primaryBackgroundColor)
}
}
private var resetSection: some View {
Section {
Button {
theme.followSystemColorScheme = true
theme.selectedSet = colorScheme == .dark ? .iceCubeDark : .iceCubeLight
theme.avatarShape = .rounded
theme.avatarPosition = .top
theme.statusActionsDisplay = .full
localValues.tintColor = theme.tintColor
localValues.primaryBackgroundColor = theme.primaryBackgroundColor
localValues.secondaryBackgroundColor = theme.secondaryBackgroundColor
localValues.labelColor = theme.labelColor
} label: {
Text("settings.display.restore")
}
}
.listRowBackground(theme.primaryBackgroundColor)
}
private var themeSelectorButton: some View {
NavigationLink(destination: ThemePreviewView()) {
HStack {
Text("settings.display.section.theme")
Spacer()
Text(theme.selectedSet.rawValue)
}
}
}
}