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

SwiftUI TextField绑定对象Int值异常及UI优化求助

问题根源与修复方案

核心问题分析

你的UI问题主要源于数据未被正确观察以及默认值与占位符逻辑冲突:

  1. SetsViewModel中的session未标记为@Published,导致UI无法感知数据变化,输入后切换字段会恢复初始值。
  2. 数值字段默认值为-1,TextField会直接显示该值而非占位符;note字段因数据未被观察,绑定失效无法输入。
  3. API能接收值但UI不更新,也是因为数据变化未触发视图刷新。

完整修复代码

1. 修正数据模型与视图模型

// 遵循Swift命名规范,结构体首字母大写
struct Session: Codable {
    var session_id: Int
    var workout_id: Int
    var sets: [MySet] = []
}

// 实现Identifiable简化ForEach使用,数值字段改为可选类型(nil时显示占位符)
struct MySet: Hashable, Codable, Identifiable {
    let id = UUID()
    var exercise_id: Int
    var weight: Int? = nil
    var reps: Int? = nil
    var quality: Int? = nil
    var rep_speed: Int? = nil
    var rest_before: Int? = nil
    var note: String = ""
}

class SetsViewModel: ObservableObject {
    var tokenFetcher = TokenFetcher()
    // 标记@Published,让UI感知session变化
    @Published var session: Session
    var exercise: Exercise
    
    init(tokenFetcher: TokenFetcher = TokenFetcher(), session_id: Int, workout_id: Int, exercise: Exercise) {
        self.exercise = exercise
        self.tokenFetcher = tokenFetcher
        self.session = Session(session_id: session_id, workout_id: workout_id, sets: [])
        
        // 初始化时设置正确的exercise_id
        for _ in 1...exercise.set_number {
            session.sets.append(MySet(exercise_id: exercise.exercise_id))
        }
    }
    
    func postSets() {
        self.tokenFetcher.getToken()
        
        guard let url = URL(string: "\(self.tokenFetcher.ngrokUrl)/Set") else {
            print("Error: 无效URL")
            return
        }
        
        var urlRequest = URLRequest(url: url, timeoutInterval: 10)
        urlRequest.allHTTPHeaderFields = ["Token": self.tokenFetcher.token]
        urlRequest.httpMethod = "POST"
        // 添加Content-Type头,确保API正确解析JSON
        urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
        
        do {
            let encodedData = try JSONEncoder().encode(session)
            urlRequest.httpBody = encodedData
        } catch {
            print("编码Sets失败: \(error)")
            return
        }
        
        let task = URLSession.shared.dataTask(with: urlRequest) { [weak self] data, response, error in
            guard let self = self else { return }
            
            if let error = error {
                print("API请求错误: \(error)")
                return
            }
            
            guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
                print("无效响应状态码")
                return
            }
            
            // 可选:解析API响应
            if let data = data {
                do {
                    // 替换为你的响应模型类型
                    // let result = try JSONDecoder().decode(YourResponseModel.self, from: data)
                    print("提交成功")
                } catch {
                    print("解析响应失败: \(error)")
                }
            }
        }
        task.resume()
    }
}

2. 修正SetView(单个输入视图)

struct SetView: View {
    let setNumber: Int
    // 直接绑定单个MySet实例,避免依赖父视图模型和索引
    @Binding var set: MySet
    
    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            Text("Set \(setNumber + 1)")
                .font(.headline)
            
            HStack(spacing: 20) {
                VStack(alignment: .leading) {
                    Text("Weight")
                        .font(.caption)
                    TextField("Lbs", value: $set.weight, format: .number)
                        .textFieldStyle(.roundedBorder)
                }
                
                VStack(alignment: .leading) {
                    Text("Reps")
                        .font(.caption)
                    TextField("# Reps", value: $set.reps, format: .number)
                        .textFieldStyle(.roundedBorder)
                }
            }
            
            HStack(spacing: 20) {
                VStack(alignment: .leading) {
                    Text("Quality")
                        .font(.caption)
                    TextField("1-5", value: $set.quality, format: .number)
                        .textFieldStyle(.roundedBorder)
                }
                
                VStack(alignment: .leading) {
                    Text("Rep Speed")
                        .font(.caption)
                    TextField("1-5", value: $set.rep_speed, format: .number)
                        .textFieldStyle(.roundedBorder)
                }
            }
            
            HStack(spacing: 20) {
                VStack(alignment: .leading) {
                    Text("Rest Before")
                        .font(.caption)
                    TextField("Seconds", value: $set.rest_before, format: .number)
                        .textFieldStyle(.roundedBorder)
                }
                
                VStack(alignment: .leading) {
                    Text("Anything else")
                        .font(.caption)
                    TextField("Enter note", text: $set.note)
                        .textFieldStyle(.roundedBorder)
                }
            }
        }
        .padding()
        .background(Color(.systemGray6))
        .cornerRadius(8)
    }
}

3. 修正SetsView(列表容器视图)

struct SetsView: View {
    var workout: Workout
    var exercise: Exercise
    var session_id: Int
    // 使用@StateObject管理视图模型生命周期
    @StateObject private var setsViewModel: SetsViewModel
    
    init(workout: Workout, exercise: Exercise, session_id: Int) {
        self.workout = workout
        self.exercise = exercise
        self.session_id = session_id
        // 初始化StateObject
        _setsViewModel = StateObject(wrappedValue: SetsViewModel(session_id: session_id, workout_id: workout.workout_id, exercise: exercise))
    }
    
    var body: some View {
        ScrollView {
            VStack(spacing: 16) {
                // 遍历sets数组,获取索引和绑定
                ForEach(Array(setsViewModel.session.sets.enumerated()), id: \.element.id) { index, _ in
                    SetView(setNumber: index, set: $setsViewModel.session.sets[index])
                }
                
                submitButton
            }
            .padding()
        }
        .navigationTitle(exercise.exercise_name)
    }
    
    var submitButton: some View {
        Button("Submit") {
            setsViewModel.postSets()
        }
        .buttonStyle(.borderedProminent)
        .padding(.top)
    }
}

关键修复点说明

  1. 数据观察:
    • @Published标记session,确保数据变化触发UI刷新。
    • @StateObject在父视图管理视图模型,@ObservedObject在子视图观察模型变化。
  2. 占位符逻辑:
    • 数值字段改为Int?,nil时TextField自动显示占位符,无需额外处理。
  3. 绑定优化:
    • 子视图直接绑定单个MySet实例,代码更模块化,避免索引依赖。
  4. API请求优化:
    • 添加Content-Type头,确保API正确解析JSON。
    • 完善错误处理,便于调试。

额外代码优化建议

  • 类型安全:为API响应定义明确的模型类型,避免使用Any解析。
  • 输入验证:添加TextField输入验证(如限制数值范围1-5),提升用户体验。
  • 状态提示:提交时添加加载状态(如按钮禁用、进度指示器)。
  • 本地缓存:可考虑使用UserDefaults临时保存输入,防止意外丢失。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.05 21:20:29