buh / CompactSlider

CompactSlider is a SwiftUI control for macOS, iOS and watchOS.
MIT License
420 stars 27 forks source link

The Slider Handle does not appear in the right place. #1

Closed wynioux closed 2 years ago

wynioux commented 2 years ago

Describe the bug The Slider Handle does not appear in the right place when the value is changed with onAppear() after the view is created.

To Reproduce

  1. Create the ViewModel:

    @MainActor final class TestViewModel: ObservableObject {
    let serviceContainer: ServiceContainerProtocol
    
    ...
    
    @Published var testValue: Double = 0.0
    var maxValue: Double = 0.0
    
    ...
    
    init(serviceContainer: ServiceContainerProtocol) {
        self.serviceContainer = serviceContainer
    
       ...
    
    }
    }
  2. Create the View:

    struct TestView: View {
    @StateObject private var viewModel: TestViewModel
    
    ...
    
    init(serviceContainer: ServiceContainerProtocol) {
        self._viewModel = StateObject(wrappedValue: TestViewModel(serviceContainer: serviceContainer))
    
        ...
    
    }
    
    var body: some View {
        ...
    
        VStack(spacing: 0) {
            CompactSlider(value: $viewModel.testValue, in: 0 ... viewModel.maxValue, step: 1) {
                Text("Lehtar Payı")
                Spacer()
                Text("%\(String(format: "%.0f", viewModel.testValue))")
            }
            .compactSliderStyle(.custom)
        }
        .onAppear {
            withAnimation {
                viewModel.testValue = 50 // Value will come somewhere else. Its dynamic.
                viewModel.maxValue = 100 // Value will come somewhere else. Its dynamic.
            }
        }
    
        ...
    
    }
    }

Expected behavior The Slider Handle should appear right in the middle.

Screenshots Current Behavior: Current Behavior

Expected Behavior: Expected Behavior

Platform and version:

Workaround The Slider Handle appears in the right place when using the asyncAfter() function.

.onAppear {
    withAnimation {
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
            viewModel.testValue = 50 // Value will come somewhere else. Its dynamic.
        }

        viewModel.maxValue = 100 // Value will come somewhere else. Its dynamic.
    }
}
buh commented 2 years ago

Hey! Thank's for the report! I was on holiday and completely missed the issue. I'll check

buh commented 2 years ago

Hey @wynioux! I checked your example, it really doesn't work as expected, because SwiftUI doesn't see the difference in the updated model properties. You can help to SwiftUI to understand that the control needs to update if you will add the .id() modifier:

@MainActor class TestViewModel: ObservableObject {
    @Published var testValue = 0.0
    var maxValue = 0.0
}

struct ContentView: View {

    @StateObject private var viewModel = TestViewModel()

    var body: some View {
        CompactSlider(value: $viewModel.testValue, in: 0...viewModel.maxValue, step: 1) {
            Text("Lehtar Payı")
            Spacer()
            Text("%\(String(format: "%.0f", viewModel.testValue))")
        }
        .id(viewModel.maxValue) // ⬅️ it will force to update the view when `maxValue` changed
        .padding()
        .onAppear {
            withAnimation {
                viewModel.testValue = 50
                viewModel.maxValue = 100
            }
        }
    }
}