SwiftUI的ForEach循环在列表更改状态时不反映更改。

huangapple go评论64阅读模式
英文:

SwiftUI ForEach loop does not reflect changes when the list changes state?

问题

我仍在努力理解如何使用@State@ObservedObject@Published等属性来实现我想要做的事情。我尝试了多种不同的方式,但问题没有得到解决。

所以现在,我的主视图中有两个@State变量,以及一个嵌套在主视图中的for循环。该循环显示存储在eventViews列表中的视图,视图的框架和位置由eventBounds列表中的值指定。

struct CalendarDayView: View {
    @State var eventViews: [CalendarEventView] = []
    @State var eventBounds: [CGRect] = []
    var body: some View {
        ZStack {
            ForEach(eventViews.indices, id: \.self) { index in
                let eventView = eventViews[index]
                let bounds = eventBounds[index]
                eventView
                    .frame(width: bounds.width,
                           height: bounds.height,
                           alignment: .center)
                    .position(x: bounds.midX, y: bounds.midY)
            }
        }
    }
}

当用户更改事件或其边界时,我调用一个名为calculateEventBounds()的函数,该函数会遍历所有事件并计算我希望它们渲染的边界,根据它们的位置等。一切都运行良好,除了当我调用calculateEventBounds()时,所做的更改只有在我切换应用程序选项卡并返回到CalendarDayView后才会立即更新到UI中。我需要在调用calculateEventBounds()后立即看到更改。如何使这个for循环无效,以便可以立即在UI中看到eventViewseventBounds的更改?

这是calculateEventBounds()的简化代码:

func calculateEventBounds() {
    self.eventViews = []
    self.eventBounds = []
    let events = eventsManager.getEventsOnDay(day: Int(day))
    
    for event in events { // 执行一些复杂的计算以确定事件的边界
        self.eventViews.append(CalendarEventView(event: event))
        self.eventBounds.append(bounds) // bounds是在for循环内定义的CGRect
    }
}

我的CalendarEventView结构体是一个视图,包含了一个CalendarEvent结构体,其中包含有关CalendarEvent的信息,如名称、开始时间等。

我尝试过使用一个单独的类,符合ObservableObject协议,并将eventViewseventBounds作为@Published属性,同时将该类存储为CalendarDayView中的@ObservedObject,然后在循环中从另一个类的getter函数中获取视图,但这也没有解决我的问题。

英文:

I am still trying to understand exactly how @State, @ObservedObject, @Published, etc properties can help accomplish what I am trying to do. I have tried a variety of these in different ways with no change in my issue.

So right now I have two @State variables along with this for loop within my main view. The loop displays the views stored within the eventViews list with the frame and position specified in the eventBounds list.

struct CalendarDayView: View { 
    @State var eventViews: [CalendarEventView] = [] 
    @State var eventBounds: [CGRect] = [] 
    var body: some View { 
        ZStack { 
            ForEach(eventViews.indices, id: \.self) { index in     
                let eventView = eventViews[index] 
                let bounds =   eventBounds[index] 
                eventView          
                    .frame(width: bounds.width, 
                        height: bounds.height, 
                        alignment: .center) 
                    .position(x: bounds.midX, y: bounds.midY) 
            } 
        } 
    }                
}

When the user makes changes to the events or their bounds I call a function calculateEventBounds() which goes through all the events and calculates the bounds I want them to be rendered with according to their positioning, etc. Everything works great, except when I call calculateEventBounds(), the changes made are only updated on the UI after I switch tabs on my app and go back to the CalendarDayView. I need the changes to be seen immediately after calling calculateEventBounds().

How can I invalidate this for loop so that the changes to eventViews and eventBounds are seen immediately in the UI?

Here is simplified code for calculateEventBounds():

func calculateEventBounds() { 
    self.eventViews = [] 
    self.eventBounds = [] 
    let events = eventsManager.getEventsOnDay(day: Int(day))         
    
    for event in events { //do fancy stuff to calculate the event's bounds 
        self.eventViews.append(CalendarEventView(event: event))   
        self.eventBounds.append(bounds) //bounds is a CGRect defined within the for loop 
    }
}

My struct CalendarEventView is a view that holds a CalendarEvent struct with the info of the CalendarEvent like it's name, start time, etc.

I've tried using a separate class that conforms to ObservableObject and holds the eventViews and eventBounds as @Published properties while storing that class as an @ObservedObject in my CalendarDayView, and the for loop would get the views from a getter function of the other class but that didn't solve my issue either.

答案1

得分: 0

有两个主要的不良实践,我甚至称它们为错误:

  1. 多个数组作为数据源
  2. SwiftUI 视图作为数据源的一部分。

一个事件应该是一个包含boundsEvent结构体,还有一个用于事件数据的init方法和用于计算边界的方法(使用网格或其他智能集合视图是否更好?)。

struct Event: Identifiable {
    let id = UUID()
    var bounds: CGRect = .zero 

    init(data: ...) { ... }

    func calculateBounds() { ... }
}

然后使用一个视图模型,一个符合ObservableObject协议的类,其中包含事件的@Published属性,以及一个获取事件并创建Event实例的方法。

class EventManager: ObservableObject {
    @Published var events = [Event]()

    func getEventsOnDay(day: Int) { 
       
        let eventData = // 获取数据        
        events = eventData.map { data in
            let event = Event(data: data)
            event.calculateBounds()
            return event
        }
    }
}

在视图中创建一个EventManager的实例作为@StateObject,然后从中获取事件。

struct CalendarDayView: View { 
    @StateObject var manager = EventManager()
   
    var body: some View { 
        ZStack { 
            ForEach(manager.events) { event in
                let bounds = event.bounds 
                CalendarEventView(event: event)         
                    .frame(width: bounds.width, 
                        height: bounds.height, 
                        alignment: .center) 
                    .position(x: bounds.midX, y: bounds.midY) 
            } 
        } 
    }                
}

onAppear(或异步task)中,您可以调用获取数据的方法。

events数组的任何更改都会触发视图刷新。

英文:

There are two major bad practices, I even call them mistakes:

  1. Multiple arrays as data source
  2. SwiftUI view as part of the data source.

An event should be an Event struct containing the bounds, an init method for the event data and the method to calculate the bounds (is a grid or another intelligent collection view not a better choice?).

struct Event: Identifiable {
    let id = UUID()
    var bounds: CGRect = .zero 

    init(data: ...) { ... }

    calculateBounds() { ... }
}

Then use a View Model, a class conforming to ObservableObject – as you already tried – with a @Published property containing the events, and also a method to get the events and create Event instances

class EventManager: ObservableObject {
    @Published var events = [Event]()

    func getEventsOnDay(day: Int) { 
       
        let eventData = // get the data        
        events = eventData.map { data in
            let event = Event(data: data)
            event.calculateBounds()
            return event
        }
    }
}

In the view create an instance of EventManager as @StateObject and get the events from there.

struct CalendarDayView: View { 
    @StateObject var manager = EventManager()
   
    var body: some View { 
        ZStack { 
            ForEach(manager.events) { event in
                let bounds = event.bounds 
                CalendarEventView(event: event)         
                    .frame(width: bounds.width, 
                        height: bounds.height, 
                        alignment: .center) 
                    .position(x: bounds.midX, y: bounds.midY) 
            } 
        } 
    }                
}

In onAppear (or in asynchronous task) you can call the method to get the data.

Any change in the events array will trigger the view to refresh.

huangapple
  • 本文由 发表于 2023年7月7日 00:31:57
  • 转载请务必保留本文链接:https://go.coder-hub.com/76630879.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定