在两个视图之间传递数据

Posted

技术标签:

【中文标题】在两个视图之间传递数据【英文标题】:Passing data between two views 【发布时间】:2019-08-08 14:35:58 【问题描述】:

我想在 watchOS 6 上创建一个安静的简单应用程序,但在 Apple 更改 Xcode 11 beta 5 中的 ObjectBindig 后,我的应用程序不再运行。我只是想在两个视图之间同步数据。

所以我用新的@Published 重写了我的应用程序,但我无法真正设置它:

class UserInput: ObservableObject 

    @Published var score: Int = 0


struct ContentView: View 
    @ObservedObject var input = UserInput()
    var body: some View 
        VStack 
            Text("Hello World\(self.input.score)")
            Button(action: self.input.score += 1)
                
                    Text("Adder")
                
            NavigationLink(destination: secondScreen()) 
                Text("Next View")
            

        

    


struct secondScreen: View 
    @ObservedObject var input = UserInput()
    var body: some View 
        VStack 
            Text("Button has been pushed \(input.score)")
            Button(action: self.input.score += 1
            ) 
                Text("Adder")
            
        

    

【问题讨论】:

我知道objectWillChange 的发行说明网站会自动发出,但只有当我明确编码时,我的东西才有效。你试过吗? SwiftUI onTapGesture does not work with ObservedObject in Mac app的可能重复 你不需要一个可观察的对象,因为@State Int 也可以工作。 用更多细节编辑了我的答案。 使用superpuccio所说的或调用ContentView().environmentObject(UserInput())并将输入声明为@EnvironmentObject var input: User Input,在这种情况下,环境.environmentObject-call会将创建的对象分发给所有孩子。 【参考方案1】:

您的代码有几个错误:

1) 您没有将ContentView 放入NavigationView,因此两个视图之间的导航从未发生过。

2) 您以错误的方式使用数据绑定。如果您需要第二个视图依赖于属于第一个视图的某个状态,则需要将对该状态的绑定传递给第二个视图。在您的第一个视图和第二个视图中,您都内联创建了 @ObservedObject

@ObservedObject var input = UserInput()

所以,第一个视图和第二个视图处理两个完全不同的对象。相反,您有兴趣在视图之间共享score。让第一个视图拥有UserInput 对象,只需将绑定到分数整数传递给第二个视图。这样,两个视图都将使用相同的值(您可以复制粘贴下面的代码并自己尝试)。

import SwiftUI

class UserInput: ObservableObject 
    @Published var score: Int = 0


struct ContentView: View 
    @ObservedObject var input = UserInput()
    var body: some View 
        NavigationView 
            VStack 
                Text("Hello World\(self.input.score)")
                Button(action: self.input.score += 1)
                    
                        Text("Adder")
                    
                NavigationLink(destination: secondScreen(score: self.$input.score)) 
                    Text("Next View")
                

            
        

    


struct secondScreen: View 
    @Binding var score:  Int
    var body: some View 
        VStack 
            Text("Button has been pushed \(score)")
            Button(action: self.score += 1
            ) 
                Text("Adder")
            
        

    


#if DEBUG
struct ContentView_Previews: PreviewProvider 
    static var previews: some View 
        ContentView()
    

#endif

如果你真的需要它,你甚至可以将整个 UserInput 对象传递给第二个视图:

import SwiftUI

class UserInput: ObservableObject 
    @Published var score: Int = 0


struct ContentView: View 
    @ObservedObject var input = UserInput() //please, note the difference between this...
    var body: some View 
        NavigationView 
            VStack 
                Text("Hello World\(self.input.score)")
                Button(action: self.input.score += 1)
                    
                        Text("Adder")
                    
                NavigationLink(destination: secondScreen(input: self.input)) 
                    Text("Next View")
                

            
        

    


struct secondScreen: View 
    @ObservedObject var input: UserInput //... and this!
    var body: some View 
        VStack 
            Text("Button has been pushed \(input.score)")
            Button(action: self.input.score += 1
            ) 
                Text("Adder")
            
        

    


#if DEBUG
struct ContentView_Previews: PreviewProvider 
    static var previews: some View 
        ContentView()
    

#endif

【讨论】:

嘿,你介意解释一下它是如何工作的吗?第一个例子,分数被传递给第二个视图。 score 是值类型。该值应复制到第二个视图。为什么会影响第一个视图?【参考方案2】:

我尝试了很多不同的方法来将数据从一个视图传递到另一个视图,并提出了一个适合简单和复杂视图/视图模型的解决方案。

版本

Apple Swift version 5.3.1 (swiftlang-1200.0.41 clang-1200.0.32.8)

此解决方案适用于 ios 14.0 以上版本,因为您需要 .onChange() 视图修饰符。该示例是在 Swift Playgrounds 中编写的。如果您需要一个 onChange 类似较低版本的修饰符,您应该编写自己的修饰符。

主视图

主视图有一个 @StateObject viewModel 处理所有视图逻辑,例如按钮点击和“数据”(testingID: String) -> 检查 ViewModel

struct TestMainView: View 
    
    @StateObject var viewModel: ViewModel = .init()
    
    var body: some View 
        VStack 
            Button(action:  self.viewModel.didTapButton() ) 
                Text("TAP")
            
            Spacer()
            SubView(text: $viewModel.testingID)
        .frame(width: 300, height: 400)
    
    

主视图模型(ViewModel)

viewModel 发布testID: String?。这个 testID 可以是任何类型的对象(例如配置对象 a.s.o,你可以命名它),对于这个例子,它只是子视图中也需要的一个字符串。

final class ViewModel: ObservableObject 
    
    @Published var testingID: String?
    
    func didTapButton() 
        self.testingID = UUID().uuidString
    
    

因此,通过点击按钮,我们的ViewModel 将更新testID。我们还希望 SubView 中的 testID 发生变化,如果它发生变化,我们还希望 SubView 能够识别和处理这些变化。通过ViewModel @Published var testingID,我们能够发布对我们视图的更改。现在让我们看看我们的 SubViewSubViewModel

子视图

所以SubView 有自己的@StateObject 来处理自己的逻辑。它与其他视图和 ViewModel 完全分离。在此示例中,SubView 仅显示来自其MainView 的 testID。但请记住,它可以是任何类型的对象,例如数据库请求的预设和配置。

struct SubView: View 
    
    @StateObject var viewModel: SubviewModel = .init()
    
    @Binding var test: String?
    init(text: Binding<String?>) 
        self._test = text
    
    
    var body: some View 
        Text(self.viewModel.subViewText ?? "no text")
            .onChange(of: self.test)  (text) in
                self.viewModel.updateText(text: text)
            
            .onAppear(perform:  self.viewModel.updateText(text: test) )
    

为了“连接”我们的testingID 发布我们的MainViewModel,我们用@Binding 初始化我们的SubView。所以现在我们的SubView 中有相同的testingID。但是我们不想直接在视图中使用它,而是需要将数据传递到我们的SubViewModel,记住我们的SubViewModel 是一个@StateObject 来处理所有的逻辑。而且我们不能在视图初始化期间将值传递给我们的@StateObject。此外,如果 MainViewModel 中的数据 (testingID: String) 发生变化,我们的 SubViewModel 应该能够识别并处理这些变化。

因此我们使用了两个ViewModifiers

onChange

.onChange(of: self.test)  (text) in
                self.viewModel.updateText(text: text)
            

onChange 修饰符订阅我们的@Binding 属性中的更改。因此,如果它更改,这些更改将传递给我们的SubViewModel。请注意,您的属性需要Equatable。如果您传递更复杂的对象,例如 Struct,请确保在您的 Struct实现此协议。

onAppear

我们需要onAppear 来处理“第一个初始数据”,因为 onChange 在您的视图第一次初始化时不会触发。它仅适用于更改

.onAppear(perform:  self.viewModel.updateText(text: test) )

好的,这里是 SubViewModel,我猜没有什么可以解释的了。

class SubviewModel: ObservableObject 
    
    @Published var subViewText: String?
    
    func updateText(text: String?) 
        self.subViewText = text
    

现在您的数据在您的 MainViewModelSubViewModel 之间是同步的,并且这种方法适用于具有许多子视图和这些子视图的子视图等的大视图。它还使您的视图和相应的视图模型具有高度的可重用性。

工作示例

GitHub 上的游乐场: https://github.com/luca251117/PassingDataBetweenViewModels

附加说明

为什么我使用onAppearonChange 而不仅仅是onReceive:似乎用onReceive 替换这两个修饰符会导致连续数据流多次触发SubViewModel updateText。如果您需要流式传输数据以进行演示,这可能很好,但如果您想处理网络呼叫,例如,这可能会导致问题。这就是为什么我更喜欢“双修饰法”的原因。

个人注意:请不要在对应视图范围之外修改StateObject。即使有某种可能,也不是它的本意。

【讨论】:

【参考方案3】:

我的问题仍然与如何在两个视图之间传递数据有关,但我有一个更复杂的 JSON 数据集,并且在传递数据和初始化数据方面都遇到了问题。我有一些有用的东西,但我确信它是不正确的。这是代码。救命!!!!

/ File: simpleContentView.swift
import SwiftUI
// Following is the more complicated @ObservedObject (Buddy and class Buddies)
struct Buddy : Codable, Identifiable, Hashable 
    var id = UUID()
    var TheirNames: TheirNames
    var dob: String = ""
    var school: String = ""
    enum CodingKeys1: String, CodingKey 
        case id = "id"
        case Names = "Names"
        case dob = "dob"
        case school = "school"
    

struct TheirNames : Codable, Identifiable, Hashable 
    var id = UUID()
    var first: String = ""
    var middle: String = ""
    var last: String = ""

    enum CodingKeys2: String, CodingKey 
        case id = "id"
        case first = "first"
        case last = "last"
    


class Buddies: ObservableObject 
    @Published var items: [Buddy] 
        didSet 
            let encoder = JSONEncoder()
            if let encoded = try? encoder.encode(items) UserDefaults.standard.set(encoded, forKey: "Items")
        
    
    @Published var buddy: Buddy
    init() 
        if let items = UserDefaults.standard.data(forKey: "Items") 
            let decoder = JSONDecoder()
            if let decoded = try? decoder.decode([Buddy].self, from: items) 
                self.items = decoded
                // ??? How to initialize here
                self.buddy = Buddy(TheirNames: TheirNames(first: "c", middle: "r", last: "c"), dob: "1/1/1900", school: "hard nocks")
                return
            
        
        // ??? How to initialize here
        self.buddy = Buddy(TheirNames: TheirNames(first: "c", middle: "r", last: "c"), dob: "1/1/1900", school: "hard nocks")
        self.items = []
    


struct simpleContentView: View 
    @Environment(\.presentationMode) var presentationMode
    @State private var showingSheet = true
    @ObservedObject var buddies = Buddies()
    var body: some View 
        VStack 
            Text("Simple View")
            Button(action: self.showingSheet.toggle()) Image(systemName: "triangle")
            .sheet(isPresented: $showingSheet) 
                simpleDetailView(buddies: self.buddies, item: self.buddies.buddy)
        
    


struct simpleContentView_Previews: PreviewProvider 
    static var previews: some View 
        simpleContentView()
    

// End of File: simpleContentView.swift
// This is in a separate file: simpleDetailView.swift
import SwiftUI

struct simpleDetailView: View 
    @Environment(\.presentationMode) var presentationMode
    @ObservedObject var buddies = Buddies()
    var item: Buddy
    var body: some View 
        VStack 
            Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
            Text("First Name = \(item.TheirNames.first)")
            Button(action: self.presentationMode.wrappedValue.dismiss()) Text("return"); Image(systemName: "gobackward")
        
    

// ??? Correct way to make preview call
struct simpleDetailView_Previews: PreviewProvider 
    static var previews: some View 
        // ??? Correct way to call here
        simpleDetailView(item: Buddy(TheirNames: TheirNames(first: "", middle: "", last: ""), dob: "", school: "") )
    

// end of: simpleDetailView.swift

【讨论】:

由于您必须使用复杂的结构来解析和处理,因此使用 State 属性是不可能的,您最终必须使用 Published 来实现所需的行为。我已经在下面发布了我的答案,试试看它是否适合你。我已经修改了它以供我自己使用(即用于可编码结构。)【参考方案4】:

直接使用@State 变量将帮助您实现这一点,但如果您想使用视图模型或@Published 为两个屏幕同步该变量,您可以这样做。因为 @State 不会绑定到 @Published 属性。要实现这一点,请按照以下步骤操作。

Step1: - 创建一个委托来绑定弹出或消失的值。

 protocol BindingDelegate 
     func updateOnPop(value : Int)
 

第 2 步:- 遵循内容视图的代码库

 class UserInput: ObservableObject 
      @Published var score: Int = 0
 

 struct ContentView: View , BindingDelegate 

   @ObservedObject var input = UserInput()

   @State var navIndex : Int? = nil

   var body: some View 
       NavigationView 
          VStack 
              Text("Hello World\(self.input.score)")
              Button(action: self.input.score += 1) 
                    Text("Adder")
                

            ZStack 
                NavigationLink(destination: secondScreen(score: self.$input.score,
                                                         del: self, navIndex: $navIndex),
                                                         tag: 1, selection: $navIndex) 
                    EmptyView()
                

                Button(action: 
                    self.navIndex = 1
                ) 
                    Text("Next View")

                
            
        
    


   func updateOnPop(value: Int) 
       self.input.score = value
   

第 3 步:按照以下步骤操作 secondScreen

final class ViewModel : ObservableObject 

@Published var score : Int

   init(_ value : Int) 
       self.score = value
   


struct secondScreen: View 

@Binding var score:  Int
@Binding var navIndex : Int?

@ObservedObject private var vm : ViewModel

var delegate  : BindingDelegate?

init(score : Binding<Int>, del : BindingDelegate, navIndex : Binding<Int?>) 
    self._score = score
    self._navIndex = navIndex
    self.delegate = del
    self.vm = ViewModel(score.wrappedValue)


private var btnBack : some View  Button(action: 
    self.delegate?.updateOnPop(value: self.vm.score)
    self.navIndex = nil
) 
    HStack 
        Text("Back")
    
    


var body: some View 
    VStack 
        Text("Button has been pushed \(vm.score)")
        Button(action: 
            self.vm.score += 1
        ) 
            Text("Adder")
        
    
    .navigationBarBackButtonHidden(true)
    .navigationBarItems(leading: btnBack)

   

【讨论】:

以上是关于在两个视图之间传递数据的主要内容,如果未能解决你的问题,请参考以下文章

试图在两个视图控制器之间传递数据

如何在由 UITabBarController 分隔的两个视图控制器之间传递数据?

在两个用户控件/视图之间传递数据

如何在两个由UITabBarController分隔的视图控制器之间传递数据?

如何在两个视图控制器之间的快速应用程序中传递 json 数据

如何在 Swift 中的视图之间传递数据