Reputation: 335
I am able to create a Slider by using SwiftUI but I am not able to change the style of the slider as shown in the image(below).
Problem: I am not able to find any option in SwiftUI to change the slider style.
Note: I want to create this by using SwiftUI only. I already created this slider in Swift by using "https://github.com/daprice/iOS-Tactile-Slider"
I have tried following but it's not the solution :
1. Slider(value: .constant(0.3)).accentColor(Color.white)
2. Slider(value: $age, in: 18...20, step: 1, minimumValueLabel: Text("18"), maximumValueLabel: Text("20")) { Text("") }
3. Slider(value: $age, in: 18...20, step: 1, minimumValueLabel: Image(systemName: "18.circle"), maximumValueLabel: Image(systemName: "20.circle")) { Text("") }
How can I create a slider with the style as shown in the image using SwiftUI only?
Upvotes: 9
Views: 20243
Reputation: 1
Credit from https://stackoverflow.com/a/76224773/22404582 (gaohomway)
Here is my revised version: I added a binding so you can link the progress value, and I set a starting state for the bar width.
struct UISliderView: View {
@State var maxWidth: CGFloat = UIScreen.main.bounds.width - 32 // or you can put your slider width
// min = 0, max = 1.0
@Binding var sliderProgress: CGFloat
@State var sliderWidth: CGFloat = 0
@State var lastDragValue: CGFloat = 0
var body: some View {
ZStack(alignment: .leading, content: {
Rectangle()
.fill(.blue.opacity(0.2))
Rectangle()
.fill(.blue)
.frame(width: sliderWidth)
})
.frame(width: maxWidth, height: 38)
.cornerRadius(8)
.onAppear {
sliderWidth = maxWidth * sliderProgress
lastDragValue = sliderWidth
}
.gesture(DragGesture(minimumDistance: 0).onChanged({ value in
updateSlideWidth(translationWidth: value.translation.width)
updateSlideProgress()
}).onEnded({ _ in
updateSlideWidth(translationWidth: nil)
}))
}
private func updateSlideProgress() {
let progress = sliderWidth / maxWidth
sliderProgress = progress <= 1.0 ? progress : 1
}
private func updateSlideWidth(translationWidth: CGFloat?) {
if let translationWidth = translationWidth {
sliderWidth = translationWidth + lastDragValue
} else {
sliderWidth = sliderWidth > maxWidth ? maxWidth : sliderWidth
}
sliderWidth = sliderWidth >= 0 ? sliderWidth : 0
if translationWidth == nil {
lastDragValue = sliderWidth
}
}
}
Upvotes: 0
Reputation: 4060
This can meet your needs
let width = UIScreen.main.bounds.width
struct Home: View {
@State var maxWidth: CGFloat = width - 32
@State var sliderProgress: CGFloat = 0
@State var sliderWidth: CGFloat = 0
@State var lastDragValue: CGFloat = 0
var body: some View {
NavigationView {
VStack {
ZStack(alignment: .leading, content: {
Rectangle()
.fill(.blue.opacity(0.2))
Rectangle()
.fill(.blue)
.frame(width: sliderWidth)
})
.frame(width: maxWidth, height: 32)
.cornerRadius(35)
.overlay(alignment: .leading) {
Text("\(Int(sliderProgress * 100))%")
.fontWeight(.semibold)
.foregroundColor(.black)
.offset(x: sliderWidth - 16, y: -64)
}
.gesture(DragGesture(minimumDistance: 0).onChanged({ (value) in
let translation = value.translation
sliderWidth = translation.width + lastDragValue
sliderWidth = sliderWidth > maxWidth ? maxWidth : sliderWidth
sliderWidth = sliderWidth >= 0 ? sliderWidth : 0
let progress = sliderWidth / maxWidth
sliderProgress = progress <= 1.0 ? progress : 1
}).onEnded({ (value) in
sliderWidth = sliderWidth > maxWidth ? maxWidth : sliderWidth
// Negative Height....
sliderWidth = sliderWidth >= 0 ? sliderWidth : 0
lastDragValue = sliderWidth
}))
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.navigationTitle("Dashboard")
.navigationBarTitleDisplayMode(.inline)
}
}
}
Upvotes: 1
Reputation: 196
Here is a good solution for Custom Slider. https://swiftuirecipes.com/blog/custom-slider-in-swiftui
I can confirm that this works. In addition, if you'd want it to be snappy you should add onEnded closure the following
.onEnded({ _ in
// once the gesture ends, trigger `onEditingChanged` again
xOffset = (trackSize.width - thumbSize.width) * CGFloat(percentage)
lastOffset = xOffset
onEditingChanged?(false)
})
Upvotes: 0
Reputation: 3591
In my case, I had to customize the thumb. (ex. screen locker)
I leave an answer for a problem similar one.
LockerSlider.swift
import SwiftUI
struct LockerSlider<V>: View where V : BinaryFloatingPoint, V.Stride : BinaryFloatingPoint {
// MARK: - Value
// MARK: Private
@Binding private var value: V
private let bounds: ClosedRange<V>
private let step: V.Stride
private let length: CGFloat = 50
private let lineWidth: CGFloat = 2
@State private var ratio: CGFloat = 0
@State private var startX: CGFloat? = nil
// MARK: - Initializer
init(value: Binding<V>, in bounds: ClosedRange<V>, step: V.Stride = 1) {
_value = value
self.bounds = bounds
self.step = step
}
// MARK: - View
// MARK: Public
var body: some View {
GeometryReader { proxy in
ZStack(alignment: .leading) {
// Track
RoundedRectangle(cornerRadius: length / 2)
.foregroundColor(Color(#colorLiteral(red: 0.8039215803, green: 0.8039215803, blue: 0.8039215803, alpha: 1)))
// Thumb
Circle()
.foregroundColor(.white)
.frame(width: length, height: length)
.offset(x: (proxy.size.width - length) * ratio)
.gesture(DragGesture(minimumDistance: 0)
.onChanged({ updateStatus(value: $0, proxy: proxy) })
.onEnded { _ in startX = nil })
}
.frame(height: length)
.overlay(overlay)
.simultaneousGesture(DragGesture(minimumDistance: 0)
.onChanged({ update(value: $0, proxy: proxy) }))
.onAppear {
ratio = min(1, max(0,CGFloat(value / bounds.upperBound)))
}
}
}
// MARK: Private
private var overlay: some View {
RoundedRectangle(cornerRadius: (length + lineWidth) / 2)
.stroke(Color.gray, lineWidth: lineWidth)
.frame(height: length + lineWidth)
}
// MARK: - Function
// MARK: Private
private func updateStatus(value: DragGesture.Value, proxy: GeometryProxy) {
guard startX == nil else { return }
let delta = value.startLocation.x - (proxy.size.width - length) * ratio
startX = (length < value.startLocation.x && 0 < delta) ? delta : value.startLocation.x
}
private func update(value: DragGesture.Value, proxy: GeometryProxy) {
guard let x = startX else { return }
startX = min(length, max(0, x))
var point = value.location.x - x
let delta = proxy.size.width - length
// Check the boundary
if point < 0 {
startX = value.location.x
point = 0
} else if delta < point {
startX = value.location.x - delta
point = delta
}
// Ratio
var ratio = point / delta
// Step
if step != 1 {
let unit = CGFloat(step) / CGFloat(bounds.upperBound)
let remainder = ratio.remainder(dividingBy: unit)
if remainder != 0 {
ratio = ratio - CGFloat(remainder)
}
}
self.ratio = ratio
self.value = V(bounds.upperBound) * V(ratio)
}
}
Demo.swift
import SwiftUI
struct Demo: View {
// MARK: - Value
// MARK: Private
@State private var number = 150000.0
// MARK - View
// MARK: Public
var body: some View {
VStack(alignment: .leading, spacing: 10) {
Text("Number\n\(number)")
.bold()
.padding(.bottom, 20)
Text("OS Slider")
Slider(value: $number, in: 0...1050000, step: 0.02)
.padding(.bottom, 20)
Text("Custom Slider")
LockerSlider(value: $number, in: 0...1050000, step: 0.02)
.padding(.bottom, 20)
}
.padding(20)
}
}
Upvotes: 12
Reputation: 1284
As it turned out for me accent color is depending on the context, as well as the frame, so we don't need to handle that.
As far as the control goes I made a really dummy and simple example. Please do not consider this as a solution, rather a starter.
struct CustomView: View {
@Binding var percentage: Float // or some value binded
var body: some View {
GeometryReader { geometry in
// TODO: - there might be a need for horizontal and vertical alignments
ZStack(alignment: .leading) {
Rectangle()
.foregroundColor(.gray)
Rectangle()
.foregroundColor(.accentColor)
.frame(width: geometry.size.width * CGFloat(self.percentage / 100))
}
.cornerRadius(12)
.gesture(DragGesture(minimumDistance: 0)
.onChanged({ value in
// TODO: - maybe use other logic here
self.percentage = min(max(0, Float(value.location.x / geometry.size.width * 100)), 100)
}))
}
}
}
You can use it like
@State var percentage: Float = 50
...
var body: some View {
...
CustomView(percentage: $percentage)
.accentColor(.red)
.frame(width: 200, height: 44)
...
Upvotes: 22