在两个视图之间传递数据

时间:2019-08-08 14:35:58

标签: swiftui xcode11 ios13

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

所以我用新的@Published重写了我的App,但是我真的不能设置它:

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")
            }
        }

    }
}

4 个答案:

答案 0 :(得分:3)

您的代码有几个错误:

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

答案 1 :(得分:0)

我的问题仍然与如何在两个视图之间传递数据有关,但是我有一个更复杂的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

答案 2 :(得分:0)

直接使用@State变量将帮助您实现此目的,但是如果您想使用视图模型或@Published在两个屏幕上同步该变量,则可以执行此操作。由于@State不会绑定到@Published属性。为此,请按照以下步骤操作。

第1步:-创建一个委托来绑定弹出或消失时的值。

 protocol BindingDelegate {
     func updateOnPop(value : Int)
 }

步骤2:-按照Content View的代码库

 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)

   }
}

答案 3 :(得分:0)

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

版本

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。我们还希望testID中的这个SubView,并且如果它发生变化,我们也希望SubView能够识别并处理这些变化。通过ViewModel @Published var testingID,我们可以发布对视图的更改。现在,让我们看一下 SubView SubViewModel

SubView

因此SubView有自己的@StateObject来处理自己的逻辑。它与其他视图和ViewModels完全分开。在此示例中,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 已发布,我们用SubView初始化@Binding。因此,现在我们在testingID中拥有相同的SubView。但是我们不想直接在视图中使用它,相反,我们需要将数据传递到SubViewModel中,请记住我们的SubViewModel是@StateObject来处理所有逻辑。而且我们无法在视图初始化期间将值传递到@StateObject中。另外,如果我们的{{1}中的数据(testingID: String)发生了变化,我们的MainViewModel应该能够识别并处理这些变化。

因此,我们使用两个SubViewModel

onChange

ViewModifiers

onChange 修饰符订阅我们.onChange(of: self.test) { (text) in self.viewModel.updateText(text: text) } 属性中的更改。因此,如果更改,这些更改将传递给我们的@Binding。请注意,您的媒体资源必须平等。如果您传递了一个更复杂的对象,例如SubViewModel,请确保在您的Struct实现该协议。

在出现

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

onAppear

好,这是 SubViewModel ,我想对此没有更多解释。

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

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

工作示例

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

附加说明

为什么我使用class SubviewModel: ObservableObject { @Published var subViewText: String? func updateText(text: String?) { self.subViewText = text } } onAppear而不是仅使用onChange:看来用onReceive替换这两个修饰符会导致连续数据流触发onReceive次。如果您需要流传输数据以进行演示,则可以,但是例如,如果您想处理网络呼叫,则可能会导致问题。这就是为什么我更喜欢“双重修正法”。

个人注意:请不要在相应视图范围之外修改StateObject。即使可能,但这也不意味着什么。