myuber
@myuber

Are you sure you want to delete the question?

If your question is resolved, you may close it.

Leaving a resolved question undeleted may help others!

We hope you find it useful!

【助けてください/SwiftUI】NavigationLinkのisActiveが勝手にfalseになってしまう

解決したいこと

SwiftUIでクイズアプリを作成しています。
CoreDataから取得したクイズを出題するアプリですが、NavigationLinkから画面遷移した先でCoreDataの値を更新をすると、isActiveがfalseになり画面遷移が閉じてしまいます。

原因や解決策をご存知の方がいらっしゃりましたら、お力を貸していただきたいです。
何卒宜しくお願いします。m(_ _)m

発生している問題・エラー

エラーは出ておりません

該当するソースコード

import SwiftUI
import CoreData


struct SelectSubCategoryView: View {
    @Environment(\.managedObjectContext) private var viewContext

    // ViewModelを呼び出し
    @EnvironmentObject var ViewModel: ViewModel

    @FetchRequest var items: FetchedResults<QuizDataEntity>

    // 大カテゴリ(イニシャライザで定義)
    var selectCategory: String
    // 中カテゴリ(ScrollVoewのButtonで定義)
    @State var selectSubCategory:String = ""

    // クイズ画面に遷移するisActive
    @State var isQAActive: Bool = false
    @State var selectQuizNum:Int = 0
    @State var showingAlert: Bool = false

    @State var showQASheet = false
    @Binding var isFirstViewActive: Bool

    @State var selectQuiz: [QuizDataEntity] = []
    @State var selectQuizCopy: [QuizDataEntity] = []


//MARK: -init
    // FetchRequestをイニシャライズ
    init(selectCategory: String, isFirstViewActive: Binding<Bool>) {
        self.selectCategory = selectCategory
        self._isFirstViewActive = isFirstViewActive

        self._items = FetchRequest(entity: QuizDataEntity.entity(),
                                  sortDescriptors: [],
                              predicate: NSPredicate(format: "category == %@", selectCategory))
    }//:init


//MARK: -body
    var body: some View {
        ZStack {
            Color.white
            VStack {
                ScrollView {
                    Text("問題数:" + String(ViewModel.selectQuizNum) + "問")
                    Text("isQAActive:" + String(isQAActive))
                    Text("isFirstViewActive:" + String(isFirstViewActive))
                    Text("selectQuiz.count:" + String(self.selectQuiz.count))
                    if ViewModel.orderedCategoryTuppleList.count > 0 {

                        ForEach(0..<ViewModel.orderedCategoryTuppleList.count, id: \.self) { dataNum in
                            Button(action: {
                                self.selectSubCategory = ViewModel.orderedCategoryTuppleList[dataNum].0
                                self.selectQuiz = getSelectQuiz(selectCategory: self.selectCategory, selectSubCategory: self.selectSubCategory)
                                self.showQASheet = true
                            }){
                                VStack{
                                    CategoryRow(categoryName: ViewModel.orderedCategoryTuppleList[dataNum].0, retensionRate: Int(ViewModel.orderedCategoryTuppleList[dataNum].1))
                                        .padding(10)
                                    Divider()
                                }//:vstack
                            }//:button
                        } //:ForEach

                    } else {
                        Text("no DATA")
                    }//:if~else
                }//:scrollview
                .background(Color.white)
                // 画面表示時に登録されているすべてのクイズを呼び出して、変数に格納した後、重複を削除する
               .onAppear(){
                    //配列の初期化
                    ViewModel.categoryTuppleList = []
                    for dataNum in (0..<items.count) {
                        ViewModel.categoryTuppleList.append((String(items[dataNum].subCategory!), items[dataNum].numOfCorrect))
                    }//:for
                    // 重複の削除
                    ViewModel.orderSetCategoryTuppleList()
               }//:onappear
                // recommendBannerを押したときにシートを表示
                .actionSheet(isPresented: $showQASheet, content: {
                    ActionSheet(title: Text("出題する問題数を選択してください。"),
                                message: Text("選択した数が少ない場合は可能な問題数を出題します。"),
                                buttons:[
                                    .default(Text("5問だけ復習する")) {
                                        ViewModel.selectQuizNum = 5
                                        if ViewModel.selectQuizNum > selectQuiz.count {
                                            self.showingAlert = true
                                        } else {
                                            self.isQAActive = true
                                        }
                                        //self.selectQuizCopy = selectQuiz.shuffled()
                                        ViewModel.selectQuizList = selectQuiz.shuffled()
                                    },
                                    .default(Text("10問だけ復習する")) {
                                        ViewModel.selectQuizNum = 10
                                        if ViewModel.selectQuizNum > selectQuiz.count {
                                            self.showingAlert = true
                                        } else {
                                            self.isQAActive = true
                                        }
                                        //self.selectQuizCopy = selectQuiz.shuffled()
                                        ViewModel.selectQuizList = selectQuiz.shuffled()
                                    },
                                    .default(Text("すべての問題を復習する")) {
                                        self.isQAActive = true
                                        ViewModel.selectQuizNum = selectQuiz.count
                                        //self.selectQuizCopy = selectQuiz.shuffled()
                                        ViewModel.selectQuizList = selectQuiz.shuffled()

                                    },
                                    .cancel(Text("キャンセル"))
                                ] //buttonは表示するボタンを配列にして渡す
                    )//:actionsheet
                })
                .alert(isPresented: $showingAlert) {
                            Alert(title: Text("CHECK"),
                                  message: Text("問題数が少なかったため、全\(selectQuiz.count)問を出題します。"),
                                  dismissButton: .default(Text("OK")){
                                //OKを押したらrecomendQAスタート
                                self.isQAActive = true
                            })//:alert
                } //:alert

//MARK: -NavigationLink
                NavigationLink(destination: QA(selectCategory: self.selectCategory,
                                               selectSubCategory: self.selectSubCategory,
                                               isFirstViewActive: self.$isFirstViewActive,
                                               quizNum: ViewModel.selectQuizNum,
                                               fetchdata: ViewModel.selectQuizList/*self.selectQuizCopy*/),
                               isActive: $isQAActive) {
                    EmptyView()
                }//:navigationlink
            }//:vstack
        }//:zstack
    } //:body
} //:view

//MARK: -Preview

struct SelectSubCategoryView_Previews: PreviewProvider {
    static var previews: some View {
        SelectSubCategoryView(selectCategory: "Python", isFirstViewActive: .constant(true))
            .environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
            .environmentObject(ViewModel())
    }
}



//MARK: -func
func getSelectQuiz(selectCategory: String, selectSubCategory: String) -> [QuizDataEntity]{

    let persistenceController = PersistenceController.shared
    let context = persistenceController.container.viewContext

    let request = NSFetchRequest<QuizDataEntity>(entityName: "QuizDataEntity")
    request.sortDescriptors = [NSSortDescriptor(key: "timestamp", ascending: true)]

    let predicate = NSPredicate(format: "category == %@ and subCategory == %@", selectCategory, selectSubCategory)
    request.predicate = predicate

    do {
        let data = try context.fetch(request)
        return data
    }
    catch {
        fatalError()
    }
}

import SwiftUI
import CoreData


struct QA: View {
    @Environment(\.managedObjectContext) private var viewContext

    // ViewModelを呼び出し
    @EnvironmentObject var ViewModel: ViewModel

    //@FetchRequest var items: FetchedResults<QuizDataEntity>

    var selectCategory:String
    var selectSubCategory:String

    @State var loadingIsShow: Bool = true

    //クイズの回答後/未回答を格納する配列(falseは未回答)
    @State var submittedList: [Bool] = [false]

    //クイズデータを格納する配列
    var fetchdata: [QuizDataEntity]
    //回答する問題数
    var quizNum: Int

    @State var correctNum:Int = 0
    @State var wrongNum:Int = 0

    @Binding var isFirstViewActive: Bool


//MARK: -init
    // FetchRequestをイニシャライズ
    init(selectCategory: String, selectSubCategory: String, isFirstViewActive: Binding<Bool>, quizNum: Int, fetchdata: [QuizDataEntity]) {
        self.selectCategory = selectCategory
        self.selectSubCategory = selectSubCategory
        self._isFirstViewActive = isFirstViewActive
        self.fetchdata = fetchdata

        self.quizNum = quizNum < self.fetchdata.count ? quizNum : self.fetchdata.count

        //▼渡されたクイズの数だけ配列にfalseを追加
        //イニシャライザ用の配列を用意
        let initList: [Bool] = [Bool](repeating: false, count: self.quizNum)
        //State変数に配列を格納
        //この書き方でないとイニシャライザ内でState変数に干渉できない
        self._submittedList = State(initialValue: initList)

        print("initList:", initList)
        print("quizNum:", self.quizNum)
        print(self.fetchdata)
    }//:init



//MARK: -View
    var body: some View {
        ZStack {
            ResultView(isFirstViewActive: self.$isFirstViewActive, submittedList: self.$submittedList, loadingIsShow: self.$loadingIsShow)

            // 最終問題数のsubmittedがtrueになったら
            if self.submittedList.last! && self.loadingIsShow {
                LoadingView(placeHolder: "結果を計算中",
                            selectCategory: self.selectCategory,
                            selectSubCategory: self.selectSubCategory,
                            itemCount: fetchdata.count)
                    .onAppear(){
                        // 正答率を計算する関数
                        ViewModel.calcCorrectRate()
                        // 1秒後にLoadingViewを非表示にする
                        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                            self.loadingIsShow = false
                        }//:dispath
                    }//:onappear

            }//:if



            ForEach((0..<self.quizNum).reversed(), id: \.self) {index in
                QuizView(
                     quizData: fetchdata[index],
                     selectCategory: fetchdata[index].category!,
                     selectSubCategory: fetchdata[index].subCategory!,
                     quizNum: index+1,
                     isSubmitted: self.$submittedList[index],
                     correct: self.$correctNum,
                     wrong: self.$wrongNum
                )
                    .offset(x: self.submittedList[index] ? 1000 : 0)

            }//:foreach

        }//:zstack
        .onAppear(){
            ViewModel.resetResultView()
        }//:onappear
        .edgesIgnoringSafeArea(.all)
        .navigationBarBackButtonHidden(true) //Navigationlinkの戻るを隠す
    }//:body
}//:view

import SwiftUI

struct QuizView: View {
    @ObservedObject var quizData: QuizDataEntity
    // 保存処理に必要なコンテキスト
    @Environment(\.managedObjectContext) private var viewContext

    // ViewModelを呼び出し
    @EnvironmentObject var ViewModel: ViewModel

    var selectCategory: String
    var selectSubCategory: String

    var quizNum: Int

    @Binding var isSubmitted: Bool
    @Binding var correct: Int
    @Binding var wrong: Int

    //各問題のステータスを更新してよいか判定する変数(これがないとisSubmittedが切り替わっているタイミングで2重に更新が発生してしまう)
    @State var ChangeStatus: Bool = true
    @State var showAnswer: Bool = false

    //dateformatter
    let dateFormatter = DateFormatter()

    //3日前の日付(2日後にまたrecommendする)
    let wrongTimeStamp = Date().addingTimeInterval(-60 * 60 * 24 * 3.0)

    var body: some View {
        ZStack {
//MARK: -background
            VStack (spacing: 0){
                Rectangle()
                    .fill(Color(#colorLiteral(red: 0.2705882353, green: 0.6823529412, blue: 0.9725490196, alpha: 1)))
                    .frame(height: 100)

                Rectangle()
                    .fill(Color(#colorLiteral(red: 0.9494364858, green: 0.9437922835, blue: 0.9537749887, alpha: 1)))
            }//:vstack

//MARK: -main VStack
            //VStack (spacing: 0){
            ScrollView(.vertical, showsIndicators: false) {
                VStack (spacing: 0){
                    ZStack {
                        Rectangle()
                            .fill(Color.white)
                            .frame(height: 40)
                        HStack(alignment: .bottom) {
                            Text(String(quizNum))
                                .font(.system(size: 35))
                                .foregroundColor(Color(#colorLiteral(red: 0.2705882353, green: 0.6823529412, blue: 0.9725490196, alpha: 1)))

                            Text("問目").padding(.bottom,7)
                                .foregroundColor(Color.black)

                        }//:hstack
                    }//:zstack
                    .padding(.top, 25)

                    Rectangle()
                        .fill(Color.black)
                        .frame(height: 3)
                }
//MARK: -Question
                    ZStack {
                        Image("bg_Home")
                            .resizable()
                            .frame(height:320)
                            .frame(width: UIScreen.screenWidth)


                        // 丸角四角(2色の四角をclipShapeで丸角に変更)
                        ZStack {
                            Rectangle()
                                .fill(Color.white)

                            VStack (spacing: 0) {
                                ZStack {
                                    Rectangle()
                                        .fill(Color(#colorLiteral(red: 0.75836128, green: 0.9091015458, blue: 0.9416609406, alpha: 1)))
                                        .frame(width: UIScreen.screenWidth*0.9, height:50)

                                    Text(self.selectCategory + ">" + self.selectSubCategory)
                                        .foregroundColor(Color.black)
                                }//:zstack

                                ScrollView {
                                    Text(quizData.question!)
                                        .foregroundColor(Color.black)
                                }
                                .frame(width: UIScreen.screenWidth*0.85)

                            }//:vstack


                        }//:zstack
                        .frame(width: UIScreen.screenWidth*0.9, height:250)
                        .clipShape(RoundedRectangle(cornerRadius: 30))


                    }//:zstack

    //MARK: -correct/wrong button
                    HStack {
                        CorrectWrongButton(image: Image("correct_button_icon"),
                                           message: "正解",
                                           bgColor: Color(#colorLiteral(red: 0.2196078431, green: 0.6941176471, blue: 0.9568627451, alpha: 1)),
                                           bgColor2: Color(#colorLiteral(red: 0.2078431373, green: 0.6235294118, blue: 0.8549019608, alpha: 1)),
                                           bgColor3: Color(#colorLiteral(red: 0.1254901961, green: 0.4392156863, blue: 0.6117647059, alpha: 1)))
                            .onTapGesture {
                                if self.ChangeStatus {
                                    self.correct += 1
                                    ViewModel.resultNum["numQuestion"]! += 1
                                    ViewModel.resultNum["numCorrect"]! += 1
                                    // 前回の正解が5日前だったら
                                    if isDaysAgo(targetDate: quizData.timestamp!) {
                                        quizData.numOfCorrect += 1  //正解数を+1
                                    }
                                    quizData.timestamp = Date() //回答日時を今日の日付に更新
                                }
                                self.ChangeStatus = false  //ステータス更新を停止

                                withAnimation {
                                    self.isSubmitted = true
                                }
                                save() //クイズを保存
                                showAnswer = false
                            }//:ontapgesture

                        Spacer()

                        CorrectWrongButton(image: Image("wrong_button_icon"),
                                           message: "不正解",
                                           bgColor: Color(#colorLiteral(red: 0.937254902, green: 0.4117647059, blue: 0.5215686275, alpha: 1)),
                                           bgColor2: Color(#colorLiteral(red: 0.8705882353, green: 0.3529411765, blue: 0.4588235294, alpha: 1)),
                                           bgColor3: Color(#colorLiteral(red: 0.7254901961, green: 0.2078431373, blue: 0.3176470588, alpha: 1)))
                            .onTapGesture {
                                if self.ChangeStatus {
                                    self.wrong += 1
                                    ViewModel.resultNum["numQuestion"]! += 1
                                    ViewModel.wrongList.append(quizData)

                                    quizData.timestamp = wrongTimeStamp //回答日時を更新
                                }
                                self.ChangeStatus = false  //ステータス更新を停止

                                withAnimation {
                                    self.isSubmitted = true
                                }
                                save() //クイズを保存
                                showAnswer = false
                            }//:ontapgesture

                    }//:hstack
                    .frame(width: UIScreen.screenWidth*0.9, height:130)

    //MARK: -Answer
                    // 丸角四角(2色の四角をclipShapeで丸角に変更)
                    ZStack {
                        Rectangle()
                            .fill(Color.white)

                        VStack (spacing: 0) {
                            ZStack {
                                Rectangle()
                                    .fill(Color(#colorLiteral(red: 0.6913308501, green: 0.6872233152, blue: 0.6944896579, alpha: 1)))
                                    .frame(width: UIScreen.screenWidth*0.9, height:50)
                                Text(showAnswer == false ? "▼解答を表示する▼" : "〜解答〜")
                                    .foregroundColor(.white)
                                    .font(.system(size:28))
                                    .fontWeight(.bold)
                            }//:zstack


                            ZStack {
                                Rectangle()
                                    .fill(showAnswer == false ? Color(#colorLiteral(red: 0.6913308501, green: 0.6872233152, blue: 0.6944896579, alpha: 1)) : Color.white)

                                ScrollView {
                                    Text(showAnswer == false ? "" : quizData.answer!)
                                        .foregroundColor(Color.black)
                                }//:scrollview
                                    .frame(width: UIScreen.screenWidth*0.85)
                            }//:zstack
                        }//:vstack

                    }//:zstack
                    .frame(width: UIScreen.screenWidth*0.9, height:250)
                    .clipShape(RoundedRectangle(cornerRadius: 30))
                    .onTapGesture {
                        withAnimation {
                            self.showAnswer = true
                        }
                    }//:ontapgesture

                    Spacer()
            }//:vstack
        }//:zstack
        .edgesIgnoringSafeArea(.all)

    }//:body

    // CoreData保存処理
    private func save() {
        do {
            try viewContext.save()
        } catch {
            // Replace this implementation with code to handle the error appropriately.
            // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
            let nsError = error as NSError
            fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
        }//:do~catch
    }

    private func isDaysAgo(targetDate: Date) -> Bool {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy/MM/dd"

        let dateToday = Date() // 今日の日付
        let baseDate = 5.0       // ●日前を基準にするか
        let dateTaget = dateToday.addingTimeInterval(-60 * 60 * 24 * baseDate)

        if dateTaget >= targetDate {
            return true
        } else {
            return false
        }

    }


}//:view

自分で試したこと

  1. QuizViewのonTapGesture内のCoreDataの値更新部分をコメントアウトした場合は、問題なく処理できることを確認しています。
  2. isActiveをObserbedObjectに変更した場合は、値更新するたびに1ページ目に画面遷移し直しました。(次ページへは進みません。)
0

No Answers yet.

Your answer might help someone💌