概要
- SwiftUIで、タップすると選択肢が飛び出てくる選択ボタンを作りたかった
- アニメーションなどを色々駆使しました
- Googleカレンダーアプリの右下のボタンぽいものにしました
環境
- macOS: 13.0.1
- iOS: 16.1
- XCode: 14.1
完成品
GitHub
ソースコード
-
選択ボタンの情報を格納する
ButtonLabel
モデルを作成します。systemName
でボタンのアイコンを指定したり、label
でボタンのラベルを指定できます。ButtonLabel.swiftimport Foundation struct ButtonLabel: Identifiable { let id = UUID() let systemName: String let label: String }
-
以下はContentView。
ButtonLabel
モデルに従って、配列を作成すると使えます。@Binding
でインデックスが返ってきます。ContentView.swiftimport SwiftUI struct ContentView: View { @State var index: Int = 1 var body: some View { ZStack { Text("選択した配列のインデックス:\(index)") PopUpFloatButton( buttonLabelArray: [ ButtonLabel(systemName: "0.circle", label: "Zero"), ButtonLabel(systemName: "1.circle", label: "One"), ButtonLabel(systemName: "2.circle", label: "Two"), ButtonLabel(systemName: "3.circle", label: "Three"), ButtonLabel(systemName: "4.circle", label: "Four"), ], index: $index ) } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
-
ここから、詳細に入ります。
PopUpFloatButton.swift
とSelectCircleButton.swift
です。解説は難しいですが、簡単にいうと、ボタンを押すと、offset
モディファイアのy
の値が変わり、重なっていたボタンがずれるようになっています。PopUpFloatButton.swiftimport SwiftUI struct PopUpFloatButton: View { let buttonLabelArray: [ButtonLabel] @Binding var index : Int @State private var offset: CGFloat = 0 @State private var angle = Angle(degrees: 180) @State private var opacity: CGFloat = 0 @State private var isOpen: Bool = false @State private var isLabelDisplay: Bool = false private let width: CGFloat = 60 var body: some View { ZStack(alignment: .bottomTrailing) { Color.black .opacity(isOpen ? 0.7 : 0) .edgesIgnoringSafeArea(.all) .onTapGesture { isOpen = false } ZStack(alignment: .trailing) { // TODO: 一応動くけど、配列が空の時エラーあるよっていうattentionが残っている。 ForEach(0..<Int(buttonLabelArray.count - 1)) { num in SelectCircleButton( buttonLabel: buttonLabelArray[indexManageDict()[index]![num]], action: { isOpen = false index = indexManageDict()[index]![num] }, color: .mint, isLabelDisplay: isLabelDisplay ) .rotationEffect(angle) .offset(y: -offset * CGFloat(num + 1)) .opacity(opacity) } SelectCircleButton( buttonLabel: buttonLabelArray[index], action: { isOpen.toggle() }, color: .blue, isLabelDisplay: isLabelDisplay, width: width ) } // ZStack .padding() } // ZStack .onChange(of: isOpen) { newValue in if newValue { withAnimation(.easeInOut(duration: 0.1)) { offset = width + 5 angle = Angle(degrees: 0) isLabelDisplay = true opacity = 1 } } else { withAnimation(.easeInOut(duration: 0.1)) { offset = 0 angle = Angle(degrees: 180) isLabelDisplay = false opacity = 0 } } } } // body private func indexManageDict() -> [Int: [Int]] { // 選択肢が5つの場合、 // [0: [1,2,3,4], 1: [0,2,3,4], 2: [0,1,3,4]]・・・] // というdictuonaryを作成して返し、選択されたボタンと、選択できるボタンのindexを分けている。 var dict: [Int: [Int]] = [:] var array: [Int] = [] let num: Int = buttonLabelArray.count for i in 0...num { for j in 0...num { if i != j { array.append(j) } } dict[i] = array array = [] } return dict } } struct PopUpFloatButton_Previews: PreviewProvider { static var previews: some View { PopUpFloatButton( buttonLabelArray: [ ButtonLabel(systemName: "1.circle", label: "One"), ButtonLabel(systemName: "2.circle", label: "Two"), ButtonLabel(systemName: "3.circle", label: "Three"), ButtonLabel(systemName: "4.circle", label: "Four"), ButtonLabel(systemName: "5.circle", label: "Five"), ], index: .constant(1) ) } }
SelectCircleButton.swiftimport SwiftUI struct SelectCircleButton: View { let buttonLabel: ButtonLabel let action: () -> Void let color: Color let isLabelDisplay: Bool @State var width: CGFloat = 40 @State private var fontSize: Font = .title var body: some View { HStack { isLabelDisplay ? Text(buttonLabel.label) .foregroundColor(.white) .fontWeight(.heavy) : nil Image(systemName: buttonLabel.systemName) .resizable() .renderingMode(.template) .foregroundColor(.white) .scaledToFit() .padding(width/5) .background( Circle() .foregroundColor(color) ) .frame(width: width) } // HStack .onTapGesture() { onTapAction() } } // body private func onTapAction() { action() withAnimation(.linear(duration: 0.5)) { width = width/3 } withAnimation(.linear(duration: 0.3)) { width = width*3 } } } struct SelectCircleButton_Previews: PreviewProvider { static var previews: some View { SelectCircleButton( buttonLabel: ButtonLabel(systemName: "1.circle", label: "One"), action: { // }, color: .blue, isLabelDisplay: true ) } }
-
以上