如何让 SwiftUI 动态响应用户事件?

Posted

技术标签:

【中文标题】如何让 SwiftUI 动态响应用户事件?【英文标题】:How do I get SwiftUI to dynamically respond to user events? 【发布时间】:2019-12-10 17:36:35 【问题描述】:

我是一位经验丰富的 ios 开发人员,但对 SwiftUI 不熟悉。我正在尝试创建一个由数据模型支持的动态接口。以下说明了我正在尝试做的事情:

模型对象:

class Book : Hashable 
    static func == (lhs: Book, rhs: Book) -> Bool 
        return lhs.title == rhs.title && lhs.author == rhs.author && lhs.favorite == rhs.favorite
    

    var title : String
    var author : String
    var favorite : Bool = false

    init(title : String, author: String) 
        self.title = title
        self.author = author
    

    func hash(into hasher: inout Hasher) 
        hasher.combine(title)
        hasher.combine(author)
        hasher.combine(favorite)
    

内容视图:

struct ContentView: View 
    @State private var library = [
        Book(title: "Lord of The Rings", author: "J.R.R. Tolkien"),
        Book(title: "Harry Potter", author: "J.K. Rowling"),
        Book(title: "Under the Dome", author: "Steven King")
    ]

    var body: some View 
        NavigationView 
            MasterView(books: $library)
                .navigationBarTitle(Text("Library"))
        .navigationViewStyle(DoubleColumnNavigationViewStyle())
    


struct MasterView: View 
    @Binding var books: [Book]

    var body: some View 
        List 
            ForEach(books, id: \.self)  book in
                HStack 
                    Button(action: , label: 
                        (book.favorite ?
                            Image(systemName: "heart.fill") :
                            Image(systemName: "heart"))
                        .imageScale(.large)
                    ).onTapGesture 
                        book.favorite.toggle()
                    
                    Text("\(book.title) by \(book.author)")
                
            
        
    

结果如下:

外观正确。但是,我希望 like 按钮能够工作:我希望点击更新模型对象中的收藏变量,然后按钮本身应该相应地更改。

第一个发生,第二个没有。

我做错了什么,如何获得我想要的动态 UI 更新?

【问题讨论】:

【参考方案1】:

另一种解决方案是遵守 ObservableObject 协议,并使用 @Published 属性观察器,这样任何正在观看我们的类的视图都会注意到该属性已更改并重新加载。像这样。

class Book : Hashable, ObservableObject 
static func == (lhs: Book, rhs: Book) -> Bool 
    return lhs.title == rhs.title && lhs.author == rhs.author && lhs.favorite == rhs.favorite


var title : String
var author : String
@Published var favorite : Bool = false

init(title : String, author: String) 
    self.title = title
    self.author = author


func hash(into hasher: inout Hasher) 
    hasher.combine(title)
    hasher.combine(author)
    hasher.combine(favorite)

比为每一行创建一个单独的视图

struct Row: View 

@ObservedObject var onebook: Book

var body: some View 
    HStack 
        Button(action: , label: 
            (onebook.favorite ?
                Image(systemName: "heart.fill") :
                Image(systemName: "heart"))
                .imageScale(.large)
        ).onTapGesture 
            self.onebook.favorite.toggle()
        
        Text("\(onebook.title) by \(onebook.author)")
    




struct MasterView: View 

@Binding var books: [Book]

var body: some View 
    List 
        ForEach(books, id: \.self)  book in
            Row(onebook: book)
        
    

【讨论】:

这比其他答案更接近我想要的——数据模型支持的重点是让单一来源直接驱动 UI。显然,@Binding@State 的对应物,@ObservedObject@ObservableObject 的对应物,但是将两者混合是行不通的。【参考方案2】:

这种方法的主要问题是

@State private var library = ...

是一个引用数组,在MasterView中更改对象时不会更改,因此库的状态不会更改,因此ContentView不会刷新。

这是使这种方法起作用所需的最低限度的更改

1) 通过更改初始化程序使模型可复制(class Book 中的更改)

var title : String
var author : String
var favorite : Bool

init(title : String, author: String, favorite : Bool = false) 
    self.title = title
    self.author = author
    self.favorite = favorite

2) 强制更改图书导致更改库(更改struct MasterView

ForEach(books, id: \.self)  book in
    HStack 
        Button(action: 
            // Main idea: modification of book, must modify the library
            // which is bound to state of parent view, which result in refresh
            self.books = self.books.map  $0 != book ? $0 :
              Book(title: book.title, author: book.author, favorite: !book.favorite)
        ) 
            (book.favorite ?
                Image(systemName: "heart.fill") :
                Image(systemName: "heart"))
            .imageScale(.large)
        
        Text("\(book.title) by \(book.author)")
    

这行得通。在 Xcode 11.2 / iOS 13.2 上测试。 (如果需要我的测试环境中的完整代码,请随时通知我)。

【讨论】:

以上是关于如何让 SwiftUI 动态响应用户事件?的主要内容,如果未能解决你的问题,请参考以下文章

响应 SwiftUI 中的按键事件

ForEach 完成时执行代码 - swiftUI

JavaScript学习-事件响应,让网页交互

在 SwiftUI 中,如何通过动态变化的滚动区域来缩放 scrollView 内的内容?

如何在 SwiftUI macOS 中创建第一响应者

如何让 SKEmitterNode 在 SwiftUI 中工作?