在 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,但这样您会在一个位置出现 username
和 password
,而在另一个位置出现错误。
理想的解决方案是将其全部抽象在同一个地方。从您的视图中删除所有属性,并将其设置为:
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
}
}
我没有添加进度,因为我时间不够,但我认为这传达了这个想法。您始终可以抽象出其他视图所需的个体身份。
如果有帮助,请告诉我。
我目前正在学习 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,但这样您会在一个位置出现username
和password
,而在另一个位置出现错误。
理想的解决方案是将其全部抽象在同一个地方。从您的视图中删除所有属性,并将其设置为:
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
}
}
我没有添加进度,因为我时间不够,但我认为这传达了这个想法。您始终可以抽象出其他视图所需的个体身份。
如果有帮助,请告诉我。