在 SwiftUI 的外部函数中使用 Binding

Work with Binding inside an external function in SwiftUI

我目前正在学习 SwiftUI 并想开发自己的应用程序。我设计了一个 LoginView 和一个 LoginHandler 来处理登录背后的所有逻辑。当用户输入错误的 username/password 时,屏幕上应出现警告。我用状态变量 loginError 解决了这个问题。但现在是棘手的部分,因为我想将此变量的绑定传递给 LoginHandler 中的登录函数。看看下面的代码:


import SwiftUI

struct LoginView: View
{
    @EnvironmentObject var loginHandler: LoginHandler
    
    @State private var username: String = ""
    @State private var password: String = ""
    @State private var loginError: Bool = false
    
    ...
    
    private func login()
    {
        loginHandler.login(username: username, password: password, error: $loginError)
    }
}

我现在正在尝试更改登录函数中的错误值:

import Foundation
import SwiftUI

class LoginHandler: ObservableObject
{
    public func login(username: String, password: String, error: Binding<Bool>)
    {
        error = true
    }
}

但是我收到了错误

Cannot assign to value: 'error' is a 'let' constant

我认为这是有道理的,因为您无法编辑 swift 中的参数。我也尝试过 _error = true 因为我曾经看到下划线与绑定结合使用,但这也不起作用。

但后来我想出了一个可行的解决方案:error.wrappedValue = true。我唯一的问题是苹果开发者文档中的以下声明:

This property provides primary access to the value’s data. However, you don’t access wrappedValue directly. Instead, you use the property variable created with the @Binding attribute.

虽然我很高兴它能工作,但我想知道是否有更好的方法来解决这种情况?

更新 20.3.21:新的边缘案例

我在评论区提到了一个你不知道你的函数会被使用多少次的情况。我现在将提供一个小代码示例:

想象一下您将从后端获得的可下载文件列表 (DownloadView):

import SwiftUI

struct DownloadView: View
{
    @EnvironmentObject var downloadHandler: DownloadHandler
    
    var body: some View
    {
        VStack
        {
            ForEach(downloadHandler.getAllDownloadableFiles())
            {
                file in DownloadItemView(file: file)
            }
        }
    }
}

每个可下载的文件都有一个名称、一个简短的描述和自己的下载按钮:

import SwiftUI

struct DownloadItemView: View
{
    @EnvironmentObject var downloadHandler: DownloadHandler
    
    @State private var downloadProgress: Double = -1
    
    var file: File
    
    var body: some View
    {
        HStack
        {
            VStack
            {
                Text(file.name)
                Text(file.description)
            }
            
            Spacer()
            
            if downloadProgress < 0
            {
                // User can start Download
                Button(action: {
                    downloadFile()
                })
                {
                    Text("Download")
                }
            }
            else
            {
                // User sees download progress
                ProgressView(value: $downloadProgress)
            }
        }
    }
    
    func downloadFile()
    {
        downloadHandler.downloadFile(file: file, progress: $downloadProgress)
    }
}

现在终于 'DownloadHandler':

import Foundation
import SwiftUI

class DownloadHandler: ObservableObject
{
    public func downloadFile(file: File, progress: Binding<Double>)
    {
        // Example for changing the value
        progress = 0.5
    }
}

你也可以更新一个函数的参数,这里是一个例子,这个没有使用Binding或State,它是inout!

I am now trying to change the value of error inside my login function:

Cannot assign to value: 'error' is a 'let' constant

所以用这个方法或例子你可以!


struct ContentView: View {
    
    @State private var value: String = "Hello World!"
    
    var body: some View {
        
        Text(value)
            .padding()
        
        Button("update") {
            
            testFuction(value: &value)
        }
        
    }
}


func testFuction(value: inout String) {
    
    value += " updated!"
}

我明白你想做什么,但它会在以后导致问题,因为你在这里处理状态。现在一个解决方案是:

  • 您可以将 error 抽象为 class,但这样您会在一个位置出现 usernamepassword,而在另一个位置出现错误。

理想的解决方案是将其全部抽象在同一个地方。从您的视图中删除所有属性,并将其设置为:

   import SwiftUI

struct LoginView: View
{
    @EnvironmentObject var loginHandler: LoginHandler
    

   
   // login() <-- Call this when needed
    ...
    
    
}

然后在你的 class:

    import Foundation
    import SwiftUI
    
  @Published error: Bool = false
   var username = ""
   var password = ""

    class LoginHandler: ObservableObject
    {
        
       

       public func login()    {
           //If you can't login then throw your error here
           self.error = true
   }
    }

您唯一要做的就是更新 username 和密码`,例如您可以用这个

TextField("username", text: $loginHandler.username)
TextField("username", text: $loginHandler.password)

编辑:为边缘情况添加更新:

  import SwiftUI

struct ContentView: View {
    var body: some View {
        LazyVGrid(columns: gridModel) {
        
            ForEach(0..<20) { x in
                CustomView(id: x)
            }
        }
    }
    let gridModel = [GridItem(.adaptive(minimum: 100, maximum: 100), spacing: 10),
                     GridItem(.adaptive(minimum: 100, maximum: 100), spacing: 10),
                     GridItem(.adaptive(minimum: 100, maximum: 100), spacing: 10)
    ]
}

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

struct CustomView: View {
    @State private var downloaded = false
    @State private var progress = 0
    @ObservedObject private var viewModel = StateManager()
    let id: Int
    

    
    var body: some View {
        showAppropriateView()
    }
    @ViewBuilder private func showAppropriateView() -> some View {
        if viewModel.downloadStates[id] == true {
            VStack {
                Circle()
                    .fill(Color.blue)
                    .frame(width: 50, height: 50)
                
                
            }
        } else {
            Button("Download") {
                downloaded = true
                viewModel.saveState(of: id, downloaded)
            }
        }
    }
}

final class StateManager: ObservableObject {
    @Published var downloadStates: [Int : Bool] = [:] {
        didSet { print(downloadStates)}
    }
    
    func saveState(of id: Int,_ downloaded: Bool) {
        downloadStates[id] = downloaded
    }
}

我没有添加进度,因为我时间不够,但我认为这传达了这个想法。您始终可以抽象出其他视图所需的个体身份。

如果有帮助,请告诉我。