【助けてください/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
自分で試したこと
- QuizViewのonTapGesture内のCoreDataの値更新部分をコメントアウトした場合は、問題なく処理できることを確認しています。
- isActiveをObserbedObjectに変更した場合は、値更新するたびに1ページ目に画面遷移し直しました。(次ページへは進みません。)
0