LoginSignup
0
1

More than 1 year has passed since last update.

【SwiftUI】選択肢が飛び出てくる選択ボタンを作った

Last updated at Posted at 2023-03-30

概要

  • SwiftUIで、タップすると選択肢が飛び出てくる選択ボタンを作りたかった
  • アニメーションなどを色々駆使しました
  • Googleカレンダーアプリの右下のボタンぽいものにしました

環境

  • macOS: 13.0.1
  • iOS: 16.1
  • XCode: 14.1

完成品

  • ボタンを押すと、選択肢が出てきて、タップしたボタンが選択されます。
  • 少し回転しながらボタンが出たり入ったりするのがこだわりポイントです。

GitHub

ソースコード

  1. 選択ボタンの情報を格納するButtonLabelモデルを作成します。systemNameでボタンのアイコンを指定したり、labelでボタンのラベルを指定できます。

    ButtonLabel.swift
        import Foundation
    
        struct ButtonLabel: Identifiable {
            let id = UUID()
            let systemName: String
            let label: String
        }
    
  2. 以下はContentView。ButtonLabelモデルに従って、配列を作成すると使えます。@Bindingでインデックスが返ってきます。

    ContentView.swift
    
        import 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()
            }
        }
    
    
  3. ここから、詳細に入ります。PopUpFloatButton.swiftSelectCircleButton.swiftです。解説は難しいですが、簡単にいうと、ボタンを押すと、offsetモディファイアのyの値が変わり、重なっていたボタンがずれるようになっています。

    PopUpFloatButton.swift
    import 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.swift
    
    import 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
            )
        }
    }
    
    
  4. 以上

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1