在两个视图之间传递数据



我想在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")
}
}
}
}

您的代码有几个错误:

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

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

版本

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)-> 检查视图模型

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

}

主视图模型(视图模型)

视图模型发布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,我们能够发布对视图的更改。现在让我们看一下我们的SubViewSubViewModel

子视图

因此,SubView有自己的@StateObject来处理自己的逻辑。它与其他视图和视图模型完全分离。在此示例中,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) })
}
}

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

因此,我们使用两个ViewModifiers

在更改

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

onChange修饰符订阅我们@Binding属性中的更改。因此,如果它发生变化,这些更改将传递给我们的SubViewModel。请注意,您的财产必须是平等的。如果您传递更复杂的对象,例如Struct,请确保在Struct实现此协议。

在出现时

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

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

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

class SubviewModel: ObservableObject {

@Published var subViewText: String?

func updateText(text: String?) {
self.subViewText = text
}
}

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

工作示例

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

附加说明

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

个人注意:请不要在相应视图的范围之外修改状态对象。即使它在某种程度上是可能的,它也不是它的意思。

我的问题仍然与如何在两个视图之间传递数据有关,但我有一个更复杂的 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为两个屏幕同步该变量,则可以执行此操作。因为@State不会绑定到@Published属性。要实现此目的,请按照以下步骤操作。

步骤1: - 创建一个委托以在弹出或消失时绑定值。

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 步:按照以下步骤进行第二屏幕

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

相关内容

  • 没有找到相关文章

最新更新