AppStorage 2 次使导航链接在单击/滑动时退出 [英] AppStorage 2 times makes navigationlinks exit upon single taps/swipes

查看:33
本文介绍了AppStorage 2 次使导航链接在单击/滑动时退出的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

将 AppStorage 用于全局变量时,如果 AppStorage 被使用 2 次,导航链接将通过简单的滑动/点击手势退出.

When using AppStorage for a global variable, navigationlinks are exited with simple swipe/tap gestures if AppStorage is used 2 times.

当我按下两个按钮之一进入两个导航链接之一时,一旦进入 NavigationLink,任何一次点击/滑动都会立即退出 NavigationLink.

When I press one of two buttons to go into one of the two navigationlinks, once inside the NavigationLink, any single tap/swipe immediately exits the NavigationLink.

我要做的就是确保我可以访问全局变量quarters".我的应用中的任何地方 - 主屏幕、第一个为我提供宿舍的 NavigationLink,以及我使用宿舍的第二个 NavigationLink.

All I am trying to do is make sure that I can access the global variable "quarters" everywhere in my app - the main screen, the first NavigationLink that gives me quarters, and the second NavigationLink where I use my quarters.

这个问题仅在我开始使用以下两行代码作为全局变量quarters"时才开始.

This issue only began when I started using the below two lines of code for the global variable "quarters".

我也愿意接受任何其他方式来拥有全局变量四分之一".这个是在我问的另一个问题中提出的,似乎工作得很好,很容易而且很简单......除了这个问题.

I am also open to any other ways to have a global variable "quarters". This one was suggested in a different question I asked and seems to work really well and easily and simply ... except for this issue.

导致行为的代码:

@AppStorage("quarters")
var quarters: Int = .zero

最少的可重现代码.在下面的代码中,按一次按钮会退出任一 NavigationLink,但滑动不会退出任一 NavigationLink.

Minimal reproducible code. In the below code, a single button press exits either NavigationLink, but swipes do not exit either NavigationLink.

import SwiftUI

struct ContentView: View {
    @AppStorage("quarters")
    var quarters: Int = .zero
    var body: some View {
        NavigationView {
            Image("Clouds")
                .overlay(
                VStack(spacing: 30) {
                    Text("\(quarters)").bold()
                        .foregroundColor(.white)
                    NavigationLink(destination: Menu1(choice: "Menu1")) {
                            Image("Menu")
                    }
                    NavigationLink(destination: Menu2(choice: "Menu2")) {
                            Image("MenuGrey")
                    }
                })
        }
    }
    }

struct Menu1: View {
    @AppStorage("quarters")
    var quarters: Int = .zero
    var choice: String
    var body: some View {
        Text("\(quarters)")
        Button(action: {
            quarters += 1
        }) {
            Image("Menu")
                .scaleEffect(0.4)
                .frame(width: 305, height: 45)
        }
    }
}

struct Menu2: View {
    @AppStorage("quarters")
    var quarters: Int = .zero
    var choice: String
    var body: some View {
        Text("\(quarters)")
        Button(action: {
            quarters += 1
        }) {
            Image("MenuGrey")
                .scaleEffect(0.4)
                .frame(width: 305, height: 45)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

我想在其中使用它的主要代码.在下面的代码中,向任何方向滑动一次将退出 NavigationLink.按下某些按钮会导致它退出,而其他按钮则不会.

The main code I want to use it in. In the below code, a single swipe in any direction will exit the NavigationLink. Some button presses cause it to exit, while others don't.

下面的代码还包含了我在网上找到的这个游戏的一部分 - 该游戏还有其他.swift 文件作为游戏的一部分,包括滑动手势,但这些 .swift 文件中没有任何内容引用相关两行代码的任何部分.

The below code also contains part of this game I found online - the game has other .swift files as part of the game including swipe gestures, but nothing in those .swift files references any part of the two lines of code in question.

import SwiftUI
import UIKit
import AVKit
import AVFoundation
import Foundation

struct ContentView: View {
    static let engine = GameEngine()
    static let storage = LocalStorage()
    static let stateTracker = GameStateTracker(initialState: (storage.board ?? engine.blankBoard, storage.score))
    @ObservedObject var viewModel =  GameViewModel(engine, storage: storage, stateTracker: stateTracker)
    @AppStorage("quarters")
    var quarters: Int = .zero
    var body: some View {
        NavigationView {
            Image("Clouds")
                .overlay(
                VStack(spacing: 30) {
                    HStack {
                        Image("quarters")
                            .frame(width: 15, height: 15)
                        Text("\(quarters)").bold()
                            .frame(width: 65, height: 15)
                            .foregroundColor(Color.white)
                    }
                    NavigationLink(destination: ResultView2(choice: "MenuGrey")) {
                            Image("MenuGrey")
                    }
                    NavigationLink(destination: MyContentView(viewModel: viewModel)) {
                        Image("Menu")
                    }
                    //Gain
                    Button(action: {
                        quarters += 1
                    }) {
                        Image("QuarterMachine")
                            .scaleEffect(0.4)
                            .frame(width: 305, height: 45)
                    }
                })
        }
    }

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
}

struct MyContentView: View {
    @ObservedObject var viewModel: GameViewModel
    @State var showMenu = false
    var body: some View {
        VStack(alignment: .center, spacing: 16) {
            Header(score: viewModel.state.score, bestScore: viewModel.bestScore, menuAction: {
                self.showMenu.toggle()
            }, undoAction: {
                self.viewModel.undo()
            }, undoEnabled: self.viewModel.isUndoable)
            GoalText()
            Board(board: viewModel.state.board, addedTile: viewModel.addedTile)
            Moves(viewModel.numberOfMoves)
        }
        .frame(minWidth: .zero,
               maxWidth: .infinity,
               minHeight: .zero,
               maxHeight: .infinity,
               alignment: .center)
            .background(Color.gameBackground)
            .background(Menu())
            .background(GameOver())
            .edgesIgnoringSafeArea(.all)
    }
}
extension MyContentView {
private func Menu() -> some View {
    EmptyView().sheet(isPresented: $showMenu) {
        MenuView(newGameAction: {
            self.viewModel.reset()
            self.showMenu.toggle()
        }, resetScoreAction: {
            self.viewModel.eraseBestScore()
            self.showMenu.toggle()
        })
    }
}

private func GameOver() -> some View {
    EmptyView().sheet(isPresented: $viewModel.isGameOver) {
        GameOverView(score: self.viewModel.state.score, moves: self.viewModel.numberOfMoves) {
            self.viewModel.reset()
        }
    }
}
}

struct ResultView2: View {
    @AppStorage("quarters")
    var quarters: Int = .zero
    var choice: String
    var body: some View {
                //Gain
                Button(action: {
                    quarters += 1
                }) {
                    Image("GainQuarters")
                        .scaleEffect(0.4)
                        .frame(width: 305, height: 45)
                }
                //quarters counter
                HStack {
                    Image("quarters")
                        .frame(width: 15, height: 15)
                    Text(String(quarters)).bold()
                        .frame(width: 65, height: 15)
                        .foregroundColor(Color.white)
                }
            }
        }

下面是 GameViewModel 代码.

Below is the GameViewModel code.

import SwiftUI
import Combine
import UIKit

class GameViewModel: ObservableObject {
    
    @AppStorage("quarters")
      var quarters: Int = .zero

      @Published var result: Int = .zero

      init() {
        result = quarters // Fetch saved value at startup 
//"'self' used in property access 'quarters' before all stored properties are initialized"
      }
//"Return from initializer without initializing all stored properties"

      // User this method on you Menu view to share the same result.
      func increaseQuarters() {
        result += 1
        quarters = result // Save the new value when result increase
      }
    
//Three below "private(set) var" have a grey notice on the side that say "self.[word] not initialized"
    private(set) var engine: Engine
    private(set) var storage: Storage
    private(set) var stateTracker: StateTracker
  
    @Published var isGameOver = false
    private(set) var addedTile: (Int, Int)? = nil {
        didSet { UIImpactFeedbackGenerator().impactOccurred() }
    }
    private(set) var bestScore: Int = .zero {
        didSet { storage.save(bestScore: bestScore) }
    }
    
    var numberOfMoves: Int {
        return stateTracker.statesCount - 1
    }
    var isUndoable: Bool {
        return stateTracker.isUndoable
    }
//Line below has error "'self.state.board' not initialized" and also for self.state.score
    var state: GameState {
        didSet {
            bestScore = max(bestScore, state.score)
            storage.save(score: state.score)
            isGameOver = engine.isGameOver(state.board)
            storage.save(board: state.board)
        }
    }
    
    init(_ engine: Engine, storage: Storage, stateTracker: StateTracker) {
        self.engine = engine
        self.storage = storage
        self.stateTracker = stateTracker
        self.state = stateTracker.last
        self.bestScore = max(storage.bestScore, storage.score)
    }
    
    func start() {
        if state.board.isMatrixEmpty { reset() }
    }
    
    func addNumber() {
        let result = engine.addNumber(state.board)
        state = stateTracker.updateCurrent(with: result.newBoard)
        addedTile = result.addedTile
    }
    
    func push(_ direction: Direction) {
        let result = engine.push(state.board, to: direction)
        let boardHasChanged = !state.board.isEqual(result.newBoard)
        state = stateTracker.next(with: (result.newBoard, state.score + result.scoredPoints))
        if boardHasChanged {
            addNumber()
        }
    }
    
    func undo() {
        state = stateTracker.undo()
    }
    
    func reset() {
        state = stateTracker.reset(with: (engine.blankBoard, .zero))
        addNumber()
    }
    
    func eraseBestScore() {
        bestScore = .zero
    }
    
}

推荐答案

问题来自于您在 ContentView 中调用 @AppStorage 并使用它们在Menu1Menu2 中,当值更新时会触发到根视图的弹出导航.我不知道这是 Apple 的错误还是预期的行为,但我建议您在 FeedbackAssistant.app 中填写错误报告,让 Apple 工程师知道.

The issue comes from the fact that you call @AppStorage in the ContentView, and by using them in Menu1 and Menu2, a pop navigation to the root view is triggered when the value is updated. I don't know if it is a bug from Apple or if it is an expected behavior, but I suggest you to fill a bug report in the FeedbackAssistant.app to let Apple engineers know about it.

为了解决您的问题,我创建了一个视图模型,您将在视图之间传递该模型并在启动时读取新值 result 以检索保存的 @AppStorage 值.我添加了一个方法来在按钮点击时升级 result,然后将新值保存到 @AppStorage.这样一来,您所期望的 NavigationLink 行为就得到了修复.

To fix your issue, I have created a view model that you will pass between your views and read a new value result at startup to retrieve the saved @AppStorage value. I added a method to upgrade result on the button tap, and then save the new value to @AppStorage. By doing so, the NavigationLink behavior you expect is fixed.

这是一个新的视图模型,它将处理视图之间的逻辑并跟踪 quarters 结果:

This is the new view model that will handle the logic between views and keep track of the quarters result:

import SwiftUI

final class ContentViewModel: ObservableObject {

  @AppStorage("quarters")
  var quarters: Int = .zero

  @Published var result: Int = .zero

  init() {
    result = quarters // Fetch saved value at startup
  }

  // User this method on you Menu view to share the same result.
  func increaseQuarters() {
    result += 1
    quarters = result // Save the new value when result increase
  }
}

这是您正在使用的视图以及解决您问题的逻辑.请注意,我在您的图像下添加了一个 .resizable() 以便他们使用提供的框架:

This is the views you are using with the logic that fixes your issue. Note that I added a .resizable() under your image for them to use the provided frame:

import SwiftUI

struct ContentView: View {

  @StateObject var viewModel = ContentViewModel() // Use @StateObject, not @ObservedObject

  var body: some View {
    NavigationView {
      Image("Clouds")
        .overlay(
          VStack(spacing: 30) {
            Text("\(viewModel.result)") // Use "result" instead of "quarters" from viewModel.
              .bold()
              .foregroundColor(.white)

            NavigationLink(destination: Menu1(viewModel: viewModel)) {
              Image("Menu")
            }

            NavigationLink(destination: Menu2(viewModel: viewModel)) {
              Image("MenuGrey")
            }
          })
    }
  }
}

struct Menu1: View {

  @ObservedObject var viewModel: ContentViewModel // @ObservedObject must be used this way

  var body: some View {
    VStack(spacing: 32) {
      Text("\(viewModel.result)")  // Use "result" instead of "quarters" from viewModel.

      Button(action: { viewModel.increaseQuarters() }) {
        Image("Menu")
          .resizable() // Use Resizable on image when modifying them
          .scaleEffect(0.4)
          .frame(width: 305, height: 45)
      }
    }
  }
}

struct Menu2: View {

  @ObservedObject var viewModel: ContentViewModel // @ObservedObject must be used this way

  var body: some View {
    VStack(spacing: 32) {
      Text("\(viewModel.result)")  // Use "result" instead of "quarters" from viewModel.

      Button(action: { viewModel.increaseQuarters() }) {
        Image("MenuGrey")
          .resizable()  // User Resizable on image when modifying them
          .scaleEffect(0.4)
          .frame(width: 305, height: 45)
      }
    }
  }
}

这篇关于AppStorage 2 次使导航链接在单击/滑动时退出的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆