Search code examples
macosbindingswiftuisliderconventions

SwiftUI sliders and binding tips?


I am working on an image editing app for macOS in SwiftUI, but I feel like I have a lot of code duplication, where things should probably more elegant.

I have some sliders, and some bindings to make sure the values update and a processing method is called when the slider value has changed. Currently I have a binding for each slider:

        let vStretch = Binding<Double>(
            get: {
                self.verticalStretchLevel
            },
        
            set: {
                self.verticalStretchLevel = $0
                applyProcessing("vertical stretch")
            }
        )
        
        let straighten = Binding<Double>(
            get: {
                self.straightenLevel
            },
        
            set: {
                self.straightenLevel = $0
                applyProcessing("straighten")
            }
        )
        
        let vignette = Binding<Double>(
            get: {
                self.vignetteLevel
            },
        
            set: {
                self.vignetteLevel = $0
                applyProcessing("vignette")
            }
        )

This is ugly right? Can anyone point me to some article, site or give me some advice on how to make this right?

Thanks in advance!


Solution

  • I ended up making a view for a slider, which also has the binding:

    //
    //  SliderView.swift
    //
    //  Created by Michel Storms on 07/12/2020.
    //
    
    import SwiftUI
    
    struct SliderView: View {
        var runFilters: () -> Void // links to function from parent view
        
        let label: String
        let level: Binding<Double>
        
        var body: some View {
            if label.count == 1 {
                HStack {
                    Text(label).frame(width: sliderValueWidth)
                    Slider(value: intensity(for: level) )
                    TextField("", value: level, formatter: sliderFormatter(), onCommit: { self.runFilters() } ).frame(width: sliderValueWidth)
                }
                .onLongPressGesture{ level.wrappedValue = 0.5 ; self.runFilters() }
                .onTapGesture(count: 2, perform: { level.wrappedValue = 0.5 ; self.runFilters() })
                .frame(height: sliderTextSize)
                .font(.system(size: sliderTextSize))
            } else {
                VStack {
                    HStack{
                        Text(label)
                        Spacer()
                        TextField("", value: level, formatter: sliderFormatter(), onCommit: { self.runFilters() } ).frame(width: sliderValueWidth)
                    }
                    .frame(height: sliderTextSize)
                    .font(.system(size: sliderTextSize))
                    
                    Slider(value: intensity(for: level) ).frame(height: sliderTextSize)
                }
                .onLongPressGesture{ level.wrappedValue = 0.5 ; self.runFilters() }
                .onTapGesture(count: 2, perform: { level.wrappedValue = 0.5 ; self.runFilters() })
                .frame(height: sliderHeight)
                .font(.system(size: sliderTextSize))
            }
        }
        
        func intensity(for sliderLevel: Binding<Double>) -> Binding<Double> {
            Binding<Double>(
                get: { sliderLevel.wrappedValue },
                set: { sliderLevel.wrappedValue = $0; self.runFilters() }
            )
        }
        
        func sliderFormatter() -> NumberFormatter {
            let formatter = NumberFormatter()
            formatter.allowsFloats = true
            formatter.numberStyle = .decimal
            formatter.alwaysShowsDecimalSeparator = true
            formatter.maximumFractionDigits = 2
            formatter.minimumFractionDigits = 2
            formatter.decimalSeparator = "."
            return formatter
        }
    }
    

    ... and then display the sliders like this:

    var body: some View {
            return List {
                VStack {
                    SliderView(runFilters: self.runFilters, label: "Exposure", level: $appState.exposureLevel)
                    SliderView(runFilters: self.runFilters, label: "Contrast", level: $appState.contrastLevel)
                    SliderView(runFilters: self.runFilters, label: "Brightness", level: $appState.brightnessLevel)
                    SliderView(runFilters: self.runFilters, label: "Shadows", level: $appState.shadowsLevel)
                    SliderView(runFilters: self.runFilters, label: "Highlights", level: $appState.highlightsLevel)
                    SliderView(runFilters: self.runFilters, label: "Vibrance", level: $appState.vibranceLevel)
                    SliderView(runFilters: self.runFilters, label: "Saturation", level: $appState.saturationLevel)
                    SliderView(runFilters: self.runFilters, label: "Clarity", level: $appState.clarityLevel)
                    SliderView(runFilters: self.runFilters, label: "Black Point", level: $appState.blackpointLevel)
                    if debug {
                        SliderView(runFilters: self.runFilters, label: "DEBUG / TEST", level: $appState.debugAndTestSliderLevel)
                    }
                }
                .font(.system(size: sliderTextSize))