3

I have a List and about 1000 item in an Array, and my items update in realtime (100 updates/s). My List is very non-performant.

This my test code (I tried with List and LazyVStack):


struct PriceBoardView2 : View {
  @EnvironmentObject var loadingEnv : LoadingEnv
  @State var listUser : [UserModel]
   
  var body: some View {
    VStack(content: {
      Button(action: {
        updateData() //fake data realtime update
      }, label: {
        Text("Fake data realtime update")
      })
      List(content: {
        ForEach(listUser.indices, id: \.self) { i in
          RowPriceBoardView2(userModel: listUser[i])
        }
      })
//      ScrollView(content: {
//        LazyVStack(content: {
//          ForEach(listUser.indices, id: \.self) { i in
//            RowPriceBoardView2(userModel: listUser[i])
//          }
//        })
//      })
    })
    .onAppear(perform: {
      for i in 0..<1000 {
        listUser.append(UserModel(number: i, name: "-", age: 0))
      }
    })
  }
   
  func updateData() {
    DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.01, execute: {
      let i = Int.random(in: 0..<1000)
      self.listUser[i] = UserModel(number: i, name: "Pla pla", age: Int.random(in: 0..<1000))
      updateData()
    })
  }
}


struct UserModel {
  var number : Int
   var name : String
   var age : Int
}

struct RowPriceBoardView2: View {
  var userModel : UserModel
   
  var body: some View {
    HStack(content: {
      Text("\(userModel.number)")
      Spacer()
      Text("\(userModel.name)")
      Spacer()
      Text("\(userModel.age)")
    })
    .frame(width: .infinity, height: 30, alignment: .center)
  }
}

rmlockerd
  • 3,776
  • 2
  • 15
  • 25
  • Hi, did any of the answers solve your question? If so, please accept it by checking the box under the vote count. Otherwise, let us know if you have any problems with them. – Phil Dukhov Sep 26 '21 at 10:25

2 Answers2

1

You do a) everything on main queue, and b) too often for UI refresh. Thus the answer is - a) the data preparing should be done on background thread and b) if preparing is faster then 50ms data are needed to be combined somehow (this part app logic so is on you)

Here is a schema to separate data preparation and UI update:


  func updateData() {
    DispatchQueue.global(qos: .background).asyncAfter(deadline: DispatchTime.now() + 0.1, execute: {
      // 1) prepare in background queue
      let i = Int.random(in: 0..<1000)
      let newValue = UserModel(number: i, name: "Pla pla", age: Int.random(in: 0..<1000))

      DispatchQueue.main.async {
            // 2) update UI on main queue
            self.listUser[i] = newValue
            updateData()
        }
    })
  }

Asperi
  • 228,894
  • 20
  • 464
  • 690
0

Updating that often is really useless. Current devices are only 60 fps, so no need to update more than that.

But you don't need to hardcode some values, or detect current fps(in case new models have more fps), because there's a CADisplayLink

It's kind of a timer which fires on each frame update, that's perfect time to update your data.

I'm gonna use an ObservableObject because it's needed for CADisplayLink target. Initialize it like this:

@StateObject
var model = Model()
  1. You can update your list once per frame
class Model: ObservableObject {
    @Published
    var listUser = Array(0..<1000)
    
    init() {
        displayLink = CADisplayLink(target: self, selector: #selector(displayLinkHandler))
        displayLink.add(to: .main, forMode: .default)
    }

    private var displayLink: CADisplayLink!
    
    @objc private func displayLinkHandler() {
        let i = listUser.indices.randomElement()!
        listUser[i] = Int.random(in: 0..<1000)
    }
}
  1. If your calculations in real life are heavier than that, you should move them out of main thread and pause timer for the time of calculations, something like this:
class Model: ObservableObject {
    @Published
    var listUser = Array(0..<1000)
    
    init() {
        displayLink = CADisplayLink(target: self, selector: #selector(displayLinkHandler))
        displayLink.add(to: .main, forMode: .default)
    }
    
    private var displayLink: CADisplayLink!
    private let queue = DispatchQueue(label: "modelQueue")
    
    @objc private func displayLinkHandler() {
        displayLink.isPaused = true
        queue.async { [self] in
            var newListUser = listUser
            let i = listUser.indices.randomElement()!
            newListUser[i] = Int.random(in: 0..<1000)
            DispatchQueue.main.async {
                listUser = newListUser
                displayLink.isPaused = false
            }
        }
    }
}
  1. If you really have to recalculate more often than once per frame, you can store your data in a tmp variable and only update your view value using same CADisplayLink, like this:
class Model: ObservableObject {
    @Published
    var listUser = Array(0..<1000)
    
    init() {
        displayLink = CADisplayLink(target: self, selector: #selector(displayLinkHandler))
        displayLink.add(to: .main, forMode: .default)
        updateData()
    }
    
    private var displayLink: CADisplayLink!
    private var tmpListUser = Array(0..<1000)
    
    private func updateData() {
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.01, execute: { [self] in
            let i = Int.random(in: 0..<1000)
            self.tmpListUser[i] = Int.random(in: 0..<1000)
            updateData()
        })
    }
    
    @objc private func displayLinkHandler() {
        listUser = tmpListUser
    }
}
Phil Dukhov
  • 67,741
  • 15
  • 184
  • 220