🦋
SwiftUI: ピアノの鍵盤をレイアウトする
ピアノの鍵盤は白鍵と黒鍵がかさなっていたり、黒鍵が歯抜け状態になっていたりするため、単純にレイアウトすることが難しいです。が、工夫することでシンプルな構造でレイアウトすることが可能です。
要点
- 鍵盤の左右方向の重なりは、鍵盤の横幅を半分にしたViewを並べ、実際の描画にはViewの倍の幅でPathに描画させることで解決
- 鍵盤の奥行き方向の重なりは
zIndex()
で解決 - 全体をリサイズ可能にするために、鍵盤の大きさや全体のサイズ指定には
aspectRatio()
を用いる
KeyState
import SwiftUI
struct KeyState: Hashable, Identifiable {
enum KeyType {
case white
case black
}
let id: UUID
let keyType: KeyType
init(keyType: KeyType) {
self.id = UUID()
self.keyType = keyType
}
var marginRatio: Double {
return keyType == .white ? 0.1 : 0.3
}
var lengthRatio: Double {
return keyType == .white ? 1.0 : 0.6
}
var color: Color {
return keyType == .white ? Color.white : Color.black
}
var zIndex: Double {
return keyType == .white ? 0.0 : 1.0
}
}
KeyShape
import SwiftUI
struct KeyShape: Shape {
var keyState: KeyState
// このPathは与えられたrectの2倍の幅を描画領域として扱う
func path(in rect: CGRect) -> Path {
let w = rect.size.width
let h = rect.size.height
let m = keyState.marginRatio * w
let l = keyState.lengthRatio
let r = w / 5.0
var path = Path()
path.move(to: CGPoint(x: m, y: 0))
path.addLine(to: CGPoint(x: 2 * w - m, y: 0))
path.addLine(to: CGPoint(x: 2 * w - m, y: l * h - r))
path.addArc(center: CGPoint(x: 2 * w - m - r, y: l * h - r),
radius: r,
startAngle: Angle(degrees: 0),
endAngle: Angle(degrees: 90),
clockwise: false)
path.addLine(to: CGPoint(x: m + r, y: l * h))
path.addArc(center: CGPoint(x: m + r, y: l * h - r),
radius: r,
startAngle: Angle(degrees: 90),
endAngle: Angle(degrees: 180),
clockwise: false)
path.closeSubpath()
return path
}
}
KeyboardView
struct KeyboardView: View {
@Binding var keys: [KeyState]
var body: some View {
ZStack {
HStack(spacing: 0) {
ForEach(Array(keys.enumerated()), id: \.element) { (offset, keyState) in
KeyShape(keyState: keyState)
.fill(keyState.color)
// 1鍵盤の高さを1とした時の幅を0.16として、その半分
.aspectRatio(0.08, contentMode: .fit)
// 黒鍵はzIndexで手前に描画する
.zIndex(keyState.zIndex)
// 黒鍵の歯抜けの箇所を補うためにダミーのViewを配置する
if [4, 11].contains(offset % 12) || offset == keys.count - 1 {
Rectangle()
.aspectRatio(0.08, contentMode: .fit)
.hidden()
}
}
}
}
// 3.52 = 0.16 × (7白鍵 × 3オクターブ + ラスト1鍵盤)
.aspectRatio(3.52, contentMode: .fit)
}
}
ContentView
import SwiftUI
struct ContentView: View {
@State var keys: [KeyState]
init() {
// 1オクターブ(0~12)の範囲での黒鍵のインデックス
let blacks: [Int] = [1, 3, 6, 8, 10]
// 12鍵盤 × 3オクターブ + ラスト1鍵盤
keys = (0 ..< 37).map { i in
// インデックスを12で割った余りで白鍵と黒鍵を判断
return KeyState(keyType: blacks.contains(i % 12) ? .black : .white)
}
}
var body: some View {
KeyboardView(keys: $keys)
}
}
Discussion