Home > Net >  SwiftUI: Slider in List/ForEach behaves strangely
SwiftUI: Slider in List/ForEach behaves strangely

Time:01-02

It's very hard to explain without a recording from a second device that I don't have, but when I try to slide my slider, it will stop when my finger is definitely still moving.

I have my code posted below. I'd be happy to answer any questions and explain whatever. I'm sure it's something really simple that I should know. Any help would be very much appreciated, thanks!

import SwiftUI
    
    class SettingsViewModel: ObservableObject {
        @Published var selectedTips = [
            10.0,
            15.0,
            18.0,
            20.0,
            25.0
        ]
        
        func addTip() {
            selectedTips.append(0.0)
            selectedTips.sort()
        }
        
        func removeTip(index: Int) {
            selectedTips.remove(at: index)
            selectedTips = selectedTips.compactMap{ $0 }
        }
    }
    
    struct SettingsTipsView: View {
        @StateObject var model = SettingsViewModel()
        
        var body: some View {
            List {
                HStack {
                    Text("Edit Suggested Tips")
                        .font(.title2)
                        .fontWeight(.semibold)
                    
                    Spacer()
                    
                    if(model.selectedTips.count < 5) {
                        Button(action: { model.addTip() }, label: {
                            Image(systemName: "plus.circle.fill")
                                .renderingMode(.original)
                                .font(.title3)
                                .padding(.horizontal, 10)
                        })
                            .buttonStyle(BorderlessButtonStyle())
                    }
                }
                
                ForEach(model.selectedTips, id: \.self) { tip in
                    let i = model.selectedTips.firstIndex(of: tip)!
                    
                    //If I don't have this debug line here then the LAST slider in the list tries to force the value to 1 constantly, even if I remove the last one, the new last slider does the same. It's from a separate file but it's pretty much the same as the array above. An explanation would be great.
                    Text("\(CalculatorViewModel.suggestedTips[i])")
    
                    HStack {
                        Text("\(tip, specifier: "%.0f")%")
                        Slider(value: $model.selectedTips[i], in: 1...99, label: { Text("Label") })
                        
                        if(model.selectedTips.count > 1) {
                            Button(action: { model.removeTip(index: i) }, label: {
                                Image(systemName: "minus.circle.fill")
                                    .renderingMode(.original)
                                    .font(.title3)
                                    .padding(.horizontal, 10)
                            })
                                .buttonStyle(BorderlessButtonStyle())
                        }
                    }
                }
            }
        }
    }

CodePudding user response:

Using id: \.self within a List or ForEach is a dangerous idea in SwiftUI. The system uses it to identify what it expects to be unique elements. But, as soon as you move the slider, you have a change of ending up with a tip value that is equal to another value in the list. Then, SwiftUI gets confused about which element is which.

To fix this, you can use items with truly unique IDs. You should also try to avoid using indexes to refer to certain items in the list. I've used list bindings to avoid that issue.

struct Tip : Identifiable {
    var id = UUID()
    var tip : Double
}

class SettingsViewModel: ObservableObject {
    @Published var selectedTips : [Tip] = [
        .init(tip:10.0),
        .init(tip:15.0),
        .init(tip:18.0),
        .init(tip:20.0),
        .init(tip:25.0)
    ]
    
    func addTip() {
        selectedTips.append(.init(tip:0.0))
        selectedTips = selectedTips.sorted(by: { a, b in
            a.tip < b.tip
        })
    }
    
    func removeTip(id: UUID) {
        selectedTips = selectedTips.filter { $0.id != id }
    }
}

struct SettingsTipsView: View {
    @StateObject var model = SettingsViewModel()
    
    var body: some View {
        List {
            HStack {
                Text("Edit Suggested Tips")
                    .font(.title2)
                    .fontWeight(.semibold)
                
                Spacer()
                
                if(model.selectedTips.count < 5) {
                    Button(action: { model.addTip() }, label: {
                        Image(systemName: "plus.circle.fill")
                            .renderingMode(.original)
                            .font(.title3)
                            .padding(.horizontal, 10)
                    })
                        .buttonStyle(BorderlessButtonStyle())
                }
            }
            
            ForEach($model.selectedTips, id: \.id) { $tip in
                HStack {
                    Text("\(tip.tip, specifier: "%.0f")%")
                        .frame(width: 50) //Otherwise, the width changes while moving the slider. You could get fancier and try to use alignment guides for a more robust solution
                    Slider(value: $tip.tip, in: 1...99, label: { Text("Label") })
                    
                    if(model.selectedTips.count > 1) {
                        Button(action: { model.removeTip(id: tip.id) }, label: {
                            Image(systemName: "minus.circle.fill")
                                .renderingMode(.original)
                                .font(.title3)
                                .padding(.horizontal, 10)
                        })
                            .buttonStyle(BorderlessButtonStyle())
                    }
                }
            }
        }
    }
}

  • Related