hajimeapp
@hajimeapp

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

【SwiftUI】Buttonに自動でアニメーションが付いてしまう現象

Q&A

Closed

解決したいこと

SwiftUIにて、状態変数(@ State)の値が変更されたときに、自動でButtonにアニメーションが付く(下からフェードインするアニメーション)のですが、これはSwiftUIの仕様でしょうか?

アニメーションを停止したいので、現在は毎回Buttonの{}内にwitAnimation(nil){}を記載しています。ただ、毎回記述するのは大変なので、どこかで自動アニメーションが付かないように設定したいのですが、そのような設定方法はありますでしょうか?

0

1Answer

SwiftUIにて、状態変数(@ State)の値が変更されたときに、自動でButtonにアニメーションが付く(下からフェードインするアニメーション)のですが、これはSwiftUIの仕様でしょうか?

いいえ、通常では起こりません。

独自のモデファイヤかエクステンションを定義していると思われます。もしくは、アニメーションが付いた個別のボタンを使用しているとか。。。

該当するボタンを書いているContentViewのコードを掲示できませんか? コードをすべて掲示するのがベストですが、Buttonを書いている箇所の前後のコードだけでも。


extension Button { ・・・ }がプロジェクト内のどこかに定義されていませんか?
外部から取り込んだライブラリがあるなら、その中も探してください。

0Like

Comments

  1. @hajimeapp

    Questioner

    @nak435
    コメントいただきありがとうございます!

    コードを見ていただけるとのこと、ありがとうございます!下記全体のコードになります!(クイズアプリを作成しております)

    どうぞよろしくお願いいたします!

    import SwiftUI
    
    @main
    struct QuizApp: App {
        var body: some Scene {
            WindowGroup {
                ContentView()
            }
        }
    }
    
    import SwiftUI
    
    struct ContentView: View {
        let quizTitle: [String] = ["  縄文時代",
                                   "  弥生時代",
                                   "  古墳時代",
                                   "  飛鳥時代",
                                   "  奈良時代",
                                   "  平安時代",
                                   "  鎌倉時代",
                                   "  室町時代",
                                   "  安土桃山時代",
                                   "  江戸時代"]
        var body: some View {
            NavigationStack {
                VStack {
                    Text("日本史クイズ")
                        .frame(width: 500, height: 100)
                        .font(.largeTitle)
                        .foregroundColor(.white)
                        .background(Color("MainColor"))
                    List {
                        ForEach(0..<quizTitle.count, id: \.self) { index in
                            NavigationLink(destination: {
                                switch index {
                                case 0: JoumonView()
                                case 1: YayoiView()
                                case 2: KofunView()
                                case 3: AsukaView()
                                case 4: NaraView()
                                case 5: HeianView()
                                case 6: KamakuraView()
                                case 7: MuromachiView()
                                case 8: AduchiMomoyamaView()
                                case 9: EdoView()
                                default: EmptyView()
                                }
                            }, label: {
                                Text(quizTitle[index])
                                    .font(.title)
                                    .foregroundColor(.white)
                            })
                            .frame(width:  UIScreen.main.bounds.width / 1.5, height: 30)
                            .padding()
                            .background(Color("MainColor"))
                            .cornerRadius(5)
                            .shadow(color: .gray, radius: 0, x: 2, y: 2)
                        }
                    }
                    .listStyle(.plain)
                }
            }
        }
    }
    
    struct ContentView_Previews: PreviewProvider {
        static var previews: some View {
            ContentView()
        }
    }
    
    import SwiftUI
    
    struct JoumonView: View {
        @Environment(\.dismiss) var dismiss
        //questionNumberは端末に保存されていないため、画面遷移すると再描画され初期値に戻る
        @State var questionNumber = 0
        @State var chosenChoice = 0
        @State var choiceTapped = false
        @State var isCorrect = false
        @State var isShowCrossMark = false
        @State var isShowAnswer = false
        @State var isShowResult = false
        @State var score = 0
        var body: some View {
            ZStack {
                Color("MainColor")
                    .ignoresSafeArea()
                VStack {
                    Text(questions[questionNumber][0])
                        .frame(width:  UIScreen.main.bounds.width / 1.5)
                        .font(.title2)
                        .foregroundColor(.black)
                        .multilineTextAlignment(.center)
                        .padding()
                        .background(Color.white)
                        .cornerRadius(5)
                        .padding()
                    //改行をするためのメソッド
                        .fixedSize(horizontal: false, vertical: true)
                    //クイズの選択肢
                    ForEach(1..<5, id: \.self) { index in
                        Button {
                            //withAnimation(nil)で、選択した選択肢のボタンの文言が、タップしてから0.3秒くらいかけて下から上に上がってくるアニメーションを削除→なぜアニメーションが発生しているのかは不明
                            withAnimation(nil) {
                                chosenChoice = index
                                choiceTapped = true
                                if chosenChoice == correctChoice[questionNumber] {
                                    isCorrect = true
                                    score += 1
                                } else {
                                    isCorrect = false
                                    isShowCrossMark = true
                                }
                                DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
                                    isShowAnswer.toggle()
                                }
                            }
                        } label: {
                            Text(questions[questionNumber][index])
                                .font(.title3)
                                .foregroundColor(.black)
                                .fixedSize(horizontal: false, vertical: true)
                        }
                        .frame(width:  UIScreen.main.bounds.width / 1.5, height: 20)
                        .padding()
                        //.backgroundの引数には() -> View型のクロージャーが入るメソッドだから、{}内に書いている。Color構造体のみを引数に取る場合は()内に記載する。
                        .background {
                            // chosenChoice == indexで選んだ選択肢のみを指定
                            if chosenChoice == index {
                                if chosenChoice == correctChoice[questionNumber] {
                                    Color(.yellow)
                                } else {
                                    Color("SelectedColor")
                                }
                                //選んでいない選択肢を指定
                            } else {
                                //正解の選択肢を指定
                                if correctChoice[questionNumber] == index && isShowAnswer {
                                    Color(.yellow)
                                } else {
                                    Color(.white)
                                }
                            }
                        }
                        .cornerRadius(5)
                        .shadow(color: .gray, radius: 0, x: 2, y: 2)
                        .disabled(choiceTapped)
                    }
                    //クイズの解説文を表示
                    if isShowAnswer {
                        ZStack {
                            Color("CommentaryColor")
                            VStack {
                                Text(isCorrect ? "⭕ 正解" : "❌ 不正解")
                                    .font(.largeTitle)
                                Text(questions[questionNumber][5])
                                    .font(.title3)
                                    .foregroundColor(.black)
                                    .multilineTextAlignment(.center)
                                    .padding()
                                    .fixedSize(horizontal: false, vertical: true)
                                Button {
                            //「次のクイズへ」ボタンをタップした後すばやく同じ箇所の画面をタップすると、なぜか「2つ先のクイズのcorrectChoiceが選択された状態で解説文が表示され、それは必ず不正解判定となる」という現象が発生(例えば1問目のクイズで「次のクイズへ」をタップしてすぐに同じ箇所をタップしたら、3問目のクイズのcorrectChoiceが選択された状態で解説文が表示され、それは必ず不正解となる)。ButtonにwithAnimation(nil)を指定したら解決した。アニメーションが発生して、見えないボタンが残っていた可能性あり?
                                    withAnimation(nil) {
                                        isShowAnswer.toggle()
                                        choiceTapped = false
                                        isCorrect = false
                                        chosenChoice = 0
                                        isShowCrossMark = false
                                        if questionNumber < 9 {
                                            questionNumber += 1
                                        } else {
                                            questionNumber = 0
                                            isShowResult.toggle()
                                        }
                                    }
                                } label: {
                                    Text(questionNumber < 9 ? "次のクイズへ" : "結果を見る")
                                        .foregroundColor(.black)
                                        .padding()
                                        .background(Color.white)
                                        .cornerRadius(5)
                                        .shadow(color: .gray, radius: 0, x: 2, y: 2)
                                }
                            }
                        }
                        .cornerRadius(5)
                        .padding()
                    }
                }
                //○マークの簡易アニメーション
                if isCorrect && isShowAnswer == false {
                    Circle()
                        .stroke(Color.red, lineWidth: 20)
                        .frame(width: 200, height: 200)
                }
                //✗マークの簡易アニメーション
                if isShowCrossMark && isShowAnswer == false {
                    CrossShape()
                        .stroke(Color.blue, lineWidth: 20)
                        .frame(width: 200, height: 200)
                }
                //正解数の結果を表示
                if isShowResult {
                    ZStack {
                        Color("MainColor")
                        VStack {
                            Text("\(score)問 / 10問 正解")
                                .font(.largeTitle)
                                .padding()
                                .foregroundColor(.white)
                            Button {
                                dismiss()
                            } label: {
                                Text("クイズ一覧へ戻る")
                                    .foregroundColor(.black)
                                    .padding()
                                    .background(Color.white)
                                    .cornerRadius(5)
                                    .shadow(color: .gray, radius: 0, x: 2, y: 2)
                            }
                            .padding(.top, 30)
                        }
                    }
                }
            }
            
        }
        //選択肢の順番と同じ(例:1番目は1、2番目は2...)
        let correctChoice = [3, 1, 3, 3, 4, 1, 2, 2, 2, 2]
        //クイズの設問文と選択肢を格納
        let questions = [["第1問\n\n縄文時代の名称は何に由来するでしょう?",
                          "A:網目模様の陶器から",
                          "B:土器の製作技術から",
                          "C:縄の模様が描かれた土器から",
                          "D:縄で編まれた衣服から",
                          "解説\n\nこれは第1問の解説です。読み終えたら「次のクイズへ」を押して下さい。"],
                         ["第2問\n\n縄文時代の主な生業は何でしたか?",
                          "A:狩猟と採集",
                          "B:農業と牧畜",
                          "C:漁業と海洋貿易",
                          "D:鉱業と加工業",
                          "解説\n\nこれは第2問の解説です。読み終えたら「次のクイズへ」を押して下さい。"],
                         ["第3問\n\n縄文時代の土器はどのような特徴を持っていましたか?",
                          "A:鋳型で作られた青銅器",
                          "B:鉄器で装飾された",
                          "C:文様や縄目が描かれた",
                          "D:金属製の飾りがついていた",
                          "解説\n\nこれは第3問の解説です。読み終えたら「次のクイズへ」を押して下さい。"],
                         ["第4問\n\n縄文時代の社会はどのような特徴を持っていましたか?",
                          "A:都市国家が形成されていた",
                          "B:階級制度が厳密に存在した",
                          "C:集落ごとに自立的な共同体が形成されていた",
                          "D:奴隷制度が広く行われていた",
                          "解説\n\nこれは第4問の解説です。読み終えたら「次のクイズへ」を押して下さい。"],
                         ["第5問\n\n縄文時代の遺跡から発見されることが多い、人間や動物の形をした土製の像を何と呼ぶでしょう?",
                          "A:土人",
                          "B:縄模様",
                          "C:偶像",
                          "D:土偶",
                          "解説\n\nこれは第5問の解説です。読み終えたら「次のクイズへ」を押して下さい。"],
                         ["第6問\n\n縄文時代の終わりに次第に増えていった食料は何でしょう?",
                          "A:稲作",
                          "B:小麦栽培",
                          "C:豆類の栽培",
                          "D:トウモロコシ栽培",
                          "解説\n\nこれは第6問の解説です。読み終えたら「次のクイズへ」を押して下さい。"],
                         ["第7問\n\n縄文時代の遺跡には火焔土器が見られますが、これはどのような用途に使われたと考えられていますか?",
                          "A:料理用の調理器具",
                          "B:儀式用の祭具",
                          "C:家屋の基礎材料",
                          "D:火を起こす道具",
                          "解説\n\nこれは第7問の解説です。読み終えたら「次のクイズへ」を押して下さい。"],
                         ["第8問\n\n縄文時代の暦は何に基づいていましたか?",
                          "A:太陽暦",
                          "B:月の満ち欠け",
                          "C:農作業の周期",
                          "D:天文現象の観察",
                          "解説\n\nこれは第8問の解説です。読み終えたら「次のクイズへ」を押して下さい。"],
                         ["第9問\n\n縄文時代の人々はどのような運動を通じて生計を立てていましたか?",
                          "A:陶芸",
                          "B:狩猟",
                          "C:踊り",
                          "D:賭博",
                          "解説\n\nこれは第9問の解説です。読み終えたら「次のクイズへ」を押して下さい。"],
                         ["第10問\n\n縄文時代の終わりに始まったとされる時代は何でしょう?",
                          "A:平安時代",
                          "B:弥生時代",
                          "C:鎌倉時代",
                          "D:江戸時代",
                          "解説\n\nこれは第10問の解説です。読み終えたら「次のクイズへ」を押して下さい。"],
                         ["第10問\n\n縄文時代の終わりに始まったとされる時代は何でしょう?",
                          "A:平安時代",
                          "B:弥生時代",
                          "C:鎌倉時代",
                          "D:江戸時代",
                          "解説\n\nこれは第10問の解説です。読み終えたら「次のクイズへ」を押して下さい。"]]
    }
    
    struct CrossShape: Shape {
        func path(in rect: CGRect) -> Path {
            var path = Path()
            
            path.move(to: CGPoint(x: rect.minX, y: rect.minY))
            path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
            
            path.move(to: CGPoint(x: rect.maxX, y: rect.minY))
            path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
            
            return path
        }
    }
    
    struct JoumonView_Previews: PreviewProvider {
        static var previews: some View {
            JoumonView()
        }
    }
    


    case 1: YayoiView()
    case 2: KofunView()
    case 3: AsukaView()
    case 4: NaraView()
    case 5: HeianView()
    case 6: KamakuraView()
    case 7: MuromachiView()
    case 8: AduchiMomoyamaView()
    case 9: EdoView()
    の画面は、中身を何も記載していないため省略しています。

  2. withAnimation(nil) {}をコメントにしてみましたが、やはり、(下からフェードインする)アニメーションなど一切ありませんでした。
    何か外部のライブラリを取り込んでいませんか?
    Xcodeのプロジェクトナビゲータ(下図の赤枠)部分のスクショを貼ってもらえますか。

    scr3.png


    アニメーションが付くボタンは、「回答の選択肢」ボタン、「次のクイズへ」ボタンの両方ですか?
    「クイズ一覧へ戻る」ボタンにもアニメーションが付きますか?

  3. @hajimeapp

    Questioner

    こちらになります!

    スクリーンショット 2023-08-17 14.22.16.png

  4. @hajimeapp

    Questioner

    ちなみにいま選択肢の方のwithAnimationをコメントアウトしても、選択肢の文字が下からフェードインするアニメーションが発生しませんでした...。

    withAnimation(nil)を付け出してから今までの経緯は以下になります。

    ①:選択肢をタップすると、下から文字がフェードインするアニメーションが発生
    ②:それを解除するために選択肢ボタンにwithAnimation(nil)を設定→解決
    ③:その後 【「次のクイズへ」ボタンをタップした後すばやく同じ箇所の画面をタップすると、なぜか「2つ先のクイズのcorrectChoiceが選択された状態で解説文が表示され、それは必ず不正解判定となる】という現象が発生
    ④:試しにwithAnimation(nil)を付けてみたところ現象がストップ
    ⑤:そして今回②のwithAnimation(nil)のみを外したところ①のアニメーションは発生しない
    ⑥:④のwithAnimation(nil)を外したら変わらず③の現象は発生している

    ①の段階ではextentionや外部ライブラリ等は入れておりませんでした。また、withAnimation(nil)を付けてから減らしたコードはありません。そのため、コメント欄でご共有したコードが全てではあるのですが、アニメーションに関連するようなコードは見当たらずでして、、、

    いずれにせよ④の現象の原因もわかっておりません...。withAnimation(nil)を付けたら解決したので、アニメーション周りが原因なのかなと推察してはいますが、どのコードが原因なのかがわからない状態です...。

  5. その後 【「次のクイズへ」ボタンをタップした後すばやく同じ箇所の画面をタップすると、なぜか「2つ先のクイズのcorrectChoiceが選択された状態で解説文が表示され、それは必ず不正解判定となる】という現象が発生

    この現象は、こちらでも確認しました(withAnimation(nil) {}をコメント化後)。
    「次のクイズへ」ボタンが少し上下に動くのも確認できました。

    これは、アニメーションではなく、ボタンより上のパーツの描画内容により、ボタンを描画するY座標の位置が変化しているためですね。
    「素早くタップされた場合の動作を確認し、2つ先に飛ぶ原因を突き止め、対策する」ことで、この挙動も解消するのではないかと思います。

    下記のコードが非常に気になります。何のために、これが必要なのでしょうか?

    1秒後にisShowAnswer.toggleをトグルする処理ですが、なぜ1秒?
    トグルする条件が明確にあるはずだと思いますが、それが1秒後なのでしょうか?

    DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
                                    isShowAnswer.toggle()
                                }
    

    //○マークの簡易アニメーション//×マークの簡易アニメーションを1秒だけ表示させたいからですかね。


    もう一点、大事なことですが、
    //クイズの解説文を表示とコメントされたif isShowAnswer { }の処理です。
    SwiftUIのZ/H/VStackの中は、Viewを書くところ(View構造を定義するところ)であって、プログラムロジック(処理)を書くところではありません。
    if文がtrueの時にだけ、そのViewを表示したいのだと思いますが、elseにEmptyView()などが必要です。
    同様に//○マークの簡易アニメーション//×マークの簡易アニメーション、それ以降の//正解数の結果を表示も同様です。

  6. @hajimeapp

    Questioner

    @nak435

    ご返信いただきありがとうございます!

    //○マークの簡易アニメーション、//×マークの簡易アニメーションを1秒だけ表示させたいからですかね。

    はい、その目的になります!現状の知識だと、処理を遅らせる以外でアニメーションを作成することができず、、、

    SwiftUIのZ/H/VStackの中は、Viewを書くところ(View構造を定義するところ)であって、プログラムロジック(処理)を書くところではありません。

    なるほど!ちなみにif文の処理を用いない場合、他にどういった書き方が考えられますでしょうか?

  7. なるほど!ちなみにif文の処理を用いない場合、他にどういった書き方が考えられますでしょうか?

    例えば、↓のように visibleモデファイヤを定義して、if文の条件をそのまま .visible()に指定することで、モデファイヤ内で非表示の制御をさせる方法があります。これを使えばif文は不要になります。


    その後 【「次のクイズへ」ボタンをタップした後すばやく同じ箇所の画面をタップすると、なぜか「2つ先のクイズのcorrectChoiceが選択された状態で解説文が表示され、それは必ず不正解判定となる】という現象が発生

    この原因というか、回避策がわかりました。

    直接の原因は isShowAnswerfalseであっても「次のクイズへ」ボタンのactionが実行されるタイミングがある、ことです。(これ自体の是非は分かりません)
    回避策は、「次のクイズへ」ボタンのactionの先頭にif !isShowAnswer { return }を入れて、questionNumberをインクリメントさせないことです。

    withAnimation(nil)を入れることで、微妙にタイミングがずれて、上のタイミングが発生しなかったのだろうと想定します。

  8. @hajimeapp

    Questioner

    @nak435

    例えば、↓のように visibleモデファイヤを定義して、if文の条件をそのまま .visible()に指定することで、モデファイヤ内で非表示の制御をさせる方法があります。これを使えばif文は不要になります。

    ご共有いただきありがとうございます!あくまでもvar body: some View {} 内はView構造体の定義を行う場所であり、その表示・非表示の処理はvar body: some View {} 外に記述するんですね!たしかに可読性が上がりますね、大変学びになりました!ありがとうございます!

    直接の原因は isShowAnswerがfalseであっても「次のクイズへ」ボタンのactionが実行されるタイミングがある

    if !isShowAnswer { return }を入れたらwithAnimation(nil)なしでもバグが解消されました!ちなみに
    、isShowAnswerがfalseであっても「次のクイズへ」ボタンのactionが実行されるタイミングが生まれてしまう原因は現状だと不明、ということで合っておりますでしょうか?

    質問続きで大変恐縮ですが、ご確認いただけますと幸いです!🙇

  9. @hajimeapp

    Questioner

    【追記】
    ただ今カスタムモディファイアでやってみたのですが、非常に使いやすかったです!可読性も増しますし、何より同じモディファイアを使用するので再現性が上がりますね!

    本当に教えていただきありがとうございます!また一歩成長しました!これからはこの記述方法を積極的に使用させていただきます!

  10. ちなみに、isShowAnswerがfalseであっても「次のクイズへ」ボタンのactionが実行されるタイミングが生まれてしまう原因は現状だと不明、ということで合っておりますでしょうか?

    「Viewの生成はisShowAnswer=trueの場合ですが、ボタンが押されたタイミングでは、isShowAnswer=falseになっていた」という事実。@Stateの変数をいくつも使っているので、「そういうタイミングで(が)あった」ということでしょうね。

    もっと時間をかけて調べれば、「そのタイミング」を突き止められるかも知れませんが、そこまで意味があるか無いかです。
    visibleモデファイヤに変更しても起きるならば、調べた方がよいかも・・・。


    また、直接関係あるか分かりませんが、@Stateのフラグで、isCorrectisShowCrossMarkの使い方はスマートとは言えないので、例えば、enumで未回答・正解・不正解を定義して、answerStateなどの一つの変数にすると全体がスッキリすると思います。

    //現状
        //両方ともfalseの場合が「未回答」
        @State var isCorrect = false        //正解時: true
        @State var isShowCrossMark = false  //不正解時: true
    
    //改善案
        enum AnswerState {
            case none      //未回答
            case correct   //正解
            case incorrect //不正解
        }
        @State var answerState = AnswerState.none
    
    :       :       :
            if chosenChoice == correctChoice[questionNumber] {
                answerState = .correct
                score += 1
            } else {
                answerState = .incorrect
            }
    :       :       :
    

    ↑こうすることで、choiceTapped変数も不要になります。
    choiceTapped == trueanswerState != .noneで判定できるので。


    @Stateの変数は、Swiftの普通の変数とは異なり、Viewを変化させるために使用する変数ですから、それを意識すると良いと思います。(@State変数を操作すると、関連するViewが再描画されると考えてください。)

  11. @hajimeapp

    Questioner

    @nak435

    ご返信いただきありがとうございます!

    @Stateの変数は、Swiftの普通の変数とは異なり、Viewを変化させるために使用する変数ですから、それを意識すると良いと思います。(@State変数を操作すると、関連するViewが再描画されると考えてください。)

    なるほど!再描画の箇所が増えると、その分処理が遅れて本来ないはずのボタンがまだ残っていたりする、という可能性が出てくるわけですね!

    今回の事態を受けて、状態変数をむやみに使用するのではなく、必要なときにだけ使用するといった意識でコードを書けるようにしたいと思います!

    enumのご提案ありがとうございます!こちら参考にさせていただきます!

  12. @hajimeapp

    Questioner

    @nak435

    ただいまenumを導入して回答状況を列挙型で管理してみました!

    コードがすごくスッキリしました!この後修正する際にも変数は少ない方がコードのミスも少なくなると思うので、なんでもかんでも変数を使うのではなく、意識的に変数を減らしていきたいと思います!

    改めて今回色々ご教示いただき本当にありがとうございました!この数日間でかなり多くのことを学べ、成長することができました!

    現時点の知識量の僕からお返しできるものがなく大変恐縮ですが、成長していつか何かお返しができるように頑張ります!

  13. お役に立てて良かったです✌️

    本件、解決であればクローズしてください。

  14. @hajimeapp

    Questioner

    @nak435

    ありがとうございます!勝手にフォローさせていただきました!これからもどうぞよろしくお願いいたします!🙇

    クローズさせていただきます!

Your answer might help someone💌