🙄

2021/10/21に公開

# 丸いスライダー

この記事でいう丸いスライダー(CircleSlider)とは以下のようなものです。

## SwiftUIで実装

`Circle()``DragGesture()`を使って実装可能です。

``````import SwiftUI

struct CircleSlider: View {
@State var angle: Double = 0.5

let width: CGFloat = 250
let height: CGFloat = 250

let beginAngle: Double = 0.1
let endAngle: Double = 0.9
let minimumValue: Double = 0
let maximumValue: Double = 100

var value: Int {
let percent = (self.angle - self.beginAngle) / (self.endAngle - self.beginAngle)

var value = (maximumValue - minimumValue) * percent + minimumValue

if value < minimumValue {
value = minimumValue
}

if maximumValue < value {
value = maximumValue
}

return Int(value)
}

func onChanged(value: DragGesture.Value) {
let vector = CGVector(dx: value.location.x, dy: value.location.y)

// 中央からタップ位置までの距離
let distanceX: Double = self.width  / 2 - vector.dx
let distanceY: Double = self.height / 2 - vector.dy

// Circle()の中央からタップ位置のラジアンアークタンジェント2を求める
let radians: Double = atan2(distanceX, distanceY)

let center: Double = (self.endAngle + self.beginAngle) / 2.0

let angle: Double = center - (radians / (2.0 * .pi))

// アニメーションをつけているが、つけなくてももちろん問題ない
withAnimation(Animation.linear(duration: 0.1)){
self.angle = self.endAngle < angle ? self.endAngle : angle
}
}

var body: some View {
ZStack {
Text(String(self.value))
Circle()
.trim(from: self.beginAngle, to: self.endAngle)
.stroke(Color.black, lineWidth: 20)
.frame(width: self.width, height: self.height)
.rotationEffect(.init(degrees: 90))
.gesture(
DragGesture().onChanged(self.onChanged(value:))
)
Circle()
.trim(from: self.beginAngle, to: self.angle)
.stroke(Color.orange, lineWidth: 20)
.frame(width: self.width, height: self.height)
.rotationEffect(.init(degrees: 90))
.gesture(
DragGesture().onChanged(self.onChanged(value:))
)
}
}
}
``````

## 使用例(1)

``````let beginAngle: Double = 0.0
let endAngle: Double = 1.0
let minimumValue: Double = 0
let maximumValue: Double = 100
``````

## 使用例(2)

``````let beginAngle: Double = 0.25
let endAngle: Double = 0.75
let minimumValue: Double = 0
let maximumValue: Double = 100
``````

## 使用例(3)

エアコンなどの最低値と最大値が決まっているパターン

``````let beginAngle: Double = 0.15
let endAngle: Double = 0.85
let minimumValue: Double = 19
let maximumValue: Double = 30
``````

## つまづいたところ

• もうatan2(アークタンジェント)の使い方とかを忘れていた。(久しぶりに数学をした気分)
• スライダーがMin(0), Max(100)までいかないことがあった。
• Circleは最初から傾いている(最初に90°回転させてあげる必要がある)
• スライダーをMin(0), Max(100)にするとジェスチャーが動作しなくなる(背景のCircleにもGestureを実装で回避)

## より使いやすく

`CircleSlider`の基本は理解できたのでBinding(@State)を使えるように改良してみましょう

``````
import SwiftUI

import SwiftUI

struct ContentView: View {
@State var value: Double = 21

var body: some View {
ZStack {
VStack {
Text("\(self.value)")
Text("\(Int(self.value))")
Text("\(String(format: "%.2f", self.value))")
}

CircleSlider(self.\$value, beginAngle: 0.1, endAngle: 0.9, minimumValue: 16, maximumValue: 30)
}
}
}

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

struct CircleSlider: View {
@Binding private var bindingValue: Double

@State private var angle: Double = 0

private let width: CGFloat
private let height: CGFloat

private let beginAngle: Double
private let endAngle: Double

private let minimumValue: Double
private let maximumValue: Double

private var value: Double {
let percent: Double = (self.angle - self.beginAngle) / (self.endAngle - self.beginAngle)

var value: Double = (self.maximumValue - self.minimumValue) * percent + self.minimumValue

if value < self.minimumValue {
value = self.minimumValue
}

if self.maximumValue < value {
value = self.maximumValue
}

return value
}

private func calculateAngle(from value: Double) -> Double {
let percent: Double = (value - self.minimumValue) / (self.maximumValue - self.minimumValue)

let angle: Double = percent * (self.endAngle - self.beginAngle) + self.beginAngle

return angle
}

public init(_ binding: Binding<Double>,
width: CGFloat = 250, height: CGFloat = 250,
beginAngle: Double = 0.0, endAngle: Double = 1.0,
minimumValue: Double = 0, maximumValue: Double = 100) {
self._bindingValue = binding

self.width = width
self.height = height

self.beginAngle = beginAngle
self.endAngle = endAngle

self.minimumValue = minimumValue
self.maximumValue = maximumValue
}

private func onChanged(value: DragGesture.Value) {
let vector = CGVector(dx: value.location.x, dy: value.location.y)

// 中央からタップ位置までの距離
let distanceX: Double = self.width  / 2 - vector.dx
let distanceY: Double = self.height / 2 - vector.dy

// Circle()の中央からタップ位置のラジアンアークタンジェント2を求める
let radians: Double = atan2(distanceX, distanceY)

let center: Double = (self.endAngle + self.beginAngle) / 2.0

let angle: Double = center - (radians / (2.0 * .pi))

// アニメーションをつけているが、つけなくてももちろん問題ない
withAnimation(Animation.linear(duration: 0.1)){
self.angle = self.endAngle < angle ? self.endAngle : angle
}
}

var body: some View {
ZStack {
Circle()
.trim(from: self.beginAngle, to: self.endAngle)
.stroke(Color.black.opacity(0.2), lineWidth: 20)
.frame(width: self.width, height: self.height)
.rotationEffect(.init(degrees: 90))
.gesture(
DragGesture().onChanged(self.onChanged(value:))
)
Circle()
.trim(from: self.beginAngle, to: self.angle)
.stroke(Color.orange, lineWidth: 20)
.frame(width: self.width, height: self.height)
.rotationEffect(.init(degrees: 90))
.gesture(
DragGesture().onChanged(self.onChanged(value:))
)
}
.onChange(of: self.value) { value in
self.bindingValue = value
}
.onAppear {
self.angle = self.calculateAngle(from: self.bindingValue)
}
}
}
``````

ログインするとコメントできます