You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI中Slider值偶尔无法保存到UserDefaults的问题排查

问题:Slider值偶尔无法正确保存到UserDefaults的原因分析

在SwiftUI开发中,通过onEditingChanged回调将Slider的设置值保存到UserDefaults,但存在偶尔(约2-3次出现一次)保存失败的情况:比如设置fontSize为40.0、lineSpacing为20.0后重启应用,却加载到fontSize为8.0、lineSpacing为1.0。测试设备为iPhone 14 Pro Simulator,相关代码及日志如下:

相关代码

import Foundation
import SwiftUI

struct SliderAndUserDefaults: View {

    @StateObject var textOptions: TextOptions = .makeUserOptions()
    private let userDefault = UserDefaults.standard

    var body: some View {

        VStack {
            Slider(
                value: $textOptions.fontSize,
                in: 8...40,
                step: 1
            ) {
                Text("font size")
            } minimumValueLabel: {
                Text("8")
            } maximumValueLabel: {
                Text("40")
            } onEditingChanged: { editing in
                if !editing {
                    print("📝 save fontSize: \(textOptions.fontSize)")
                    userDefault.set(textOptions.fontSize, forKey: "fontSize")
                }
            }

            Slider(
                value: $textOptions.lineSpacing,
                in: 1...20,
                step: 1
            ) {
                Text("line spacing")
            } minimumValueLabel: {
                Text("1")
            } maximumValueLabel: {
                Text("20")
            } onEditingChanged: { editing in
                if !editing {
                    print("📝 save lineSpacing: \(textOptions.lineSpacing)")
                    userDefault.set(textOptions.lineSpacing, forKey: "lineSpacing")
                }
            }
        }
    }
}


class TextOptions: ObservableObject {
    @Published var fontSize: CGFloat
    @Published var lineSpacing: CGFloat

    static let defaultFontSize: CGFloat = 12
    static let defaultLineSpacing: CGFloat = 4

    init(fontSize: CGFloat, lineSpacing: CGFloat) {
        self.fontSize = fontSize
        self.lineSpacing = lineSpacing
    }

    static func makeUserOptions() -> TextOptions {
        let userDefaults = UserDefaults.standard
        let savedFontSize: Int = userDefaults.integer(forKey: "fontSize")
        let savedLineSpacing: Int = userDefaults.integer(forKey: "lineSpacing")

        print("📝 savedFontSize: \(savedFontSize)")
        print("📝 savedLineSpacing: \(savedLineSpacing)")

        let fontSize: CGFloat = savedFontSize == 0
        ? defaultFontSize
        : CGFloat(savedFontSize)
        let lineSpacing: CGFloat = savedLineSpacing == 0
        ? defaultLineSpacing
        : CGFloat(savedLineSpacing)

        print("📝 fontSize: \(fontSize)")
        print("📝 lineSpacing: \(lineSpacing)")

        return .init(fontSize: fontSize, lineSpacing: lineSpacing)
    }
}

重启应用日志

// change slider value
📝 save fontSize: 40.0
📝 save lineSpacing: 20.0

// restart app
📝 savedFontSize: 8
📝 savedLineSpacing: 1
📝 fontSize: 8.0
📝 lineSpacing: 1.0

问题原因分析

1. UserDefaults异步写入导致的持久化延迟

UserDefaults的set(_:forKey:)方法默认是异步写入磁盘的——调用方法后,值会先存在内存缓存中,之后再后台写入磁盘。如果在写入完成前快速重启应用,内存中的新值还没同步到磁盘,重启后就会读取到之前的旧值(比如你之前设置过的8或1),这就导致了偶尔出现的保存失败。

2. 数据类型不匹配的潜在风险

代码中存在明显的类型不一致问题:

  • 保存时,存入的是CGFloat(本质是浮点数,UserDefaults会自动转成Double存储)
  • 读取时,用userDefaults.integer(forKey:)强制读取成Int

虽然大多数情况下,Double(40.0)转成Int是40,但这种跨类型存储读取的做法,可能在某些边缘场景下触发UserDefaults的类型解析异常,比如内部存储的类型标识出错,导致读取到Slider的最小值(8和1)。


修复建议

  1. 统一数据类型:因为Slider的step是1,所有值都是整数,直接用Int类型处理:

    • 保存时,把textOptions.fontSize转成Int再存入UserDefaults
    userDefault.set(Int(textOptions.fontSize), forKey: "fontSize")
    
    • 读取时保持原逻辑即可,因为本来就是读取Int值后转成CGFloat
  2. 验证异步写入问题(调试用):如果想确认是否是异步写入导致的问题,可以在保存后调用userDefault.synchronize()(苹果不推荐生产环境使用,但调试时可以快速验证):

    userDefault.set(Int(textOptions.fontSize), forKey: "fontSize")
    userDefault.synchronize()
    

内容的提问来源于stack exchange,提问作者user9015472

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 03:25:08