Список SwiftUI очень лагает при прокрутке с данными в реальном времени (быстрое изменение элемента массива)

У меня есть список и около 1000 элементов массива, и мой элемент обновляется в реальном времени (обновляется 100 мессенджеров / с). Мой список очень сильно отстает. Я вижу, что ForEach работает в полную силу (1000 ходов)/ (1 элемент обновления) => сумасшедший. Можете ли вы помочь мне в решении вроде reloadRows в UIKit с UITableView.

Большое спасибо!

Это мой тестовый код (я пробую использовать List и 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)
  }
}

2 ответа

Вы делаете а) все в основной очереди и б) слишком часто для обновления пользовательского интерфейса. Таким образом, ответ таков: а) подготовка данных должна выполняться в фоновом потоке и б) если подготовка выполняется быстрее, необходимо как-то объединить данные за 50 мс (логика этой части приложения тоже лежит на вас)

Вот схема разделения подготовки данных и обновления пользовательского интерфейса:

      
  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()
        }
    })
  }

Часто обновлять это действительно бесполезно. Текущие устройства только 60 кадров в секунду, поэтому не нужно обновлять больше.

Но вам не нужно жестко кодировать некоторые значения или определять текущий fps (в случае, если новые модели имеют больше fps), потому что есть

Это своего рода таймер, который срабатывает при каждом обновлении кадра, это идеальное время для обновления ваших данных.

Я собираюсь использовать ObservableObjectпотому что это нужно для цели. Инициализируйте его так:

      @StateObject
var model = Model()
  1. Вы можете обновлять свой список один раз за кадр
      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. Если ваши вычисления в реальной жизни тяжелее этого, вы должны переместить их из основного потока и приостановить таймер на время вычислений, примерно так:
      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. Если вам действительно нужно пересчитывать чаще, чем один раз за кадр, вы можете сохранить свои данные в переменной tmp и обновлять только значение представления, используя то же CADisplayLink, нравится:
      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
    }
}
Другие вопросы по тегам