Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save astericky/ef947e67198af44c2a9387cd4c31e377 to your computer and use it in GitHub Desktop.
Save astericky/ef947e67198af44c2a9387cd4c31e377 to your computer and use it in GitHub Desktop.
Refreshing SwiftUI on changes within objects in a collection.

Refreshing SwiftUI on changes within objects in a collection.

How to approach updating UI for changes within objects held within a collection? After experimenting a bit, I think there are basically two approaches.

  1. The conformance to BindableObject by the object that contains the collection needs to be deep and not shallow. It needs to fire didChange if the collection, or anything recursively within the collection, changes. In my example from yesterday, this means that MainViewModel would need to figure out how to fire didChange if anything within any of its existing rows changes - not just changes to the collection itself. or
  2. The parent/container can have shallow conformance to BindableObject if the row model themselves conform to BindableObject and the row view declares the dependency with @ObjectBinding.

You must do one or the other if you want a row to refresh when isTyping changes value. I suspect that in the general case, #2 will be simpler.

This code is an example of #2.

import SwiftUI
import Combine

struct ContentView : View {
    @State var viewModel: ViewModel         // Doesn't need to State. Could be ObjectBinding, Evironment, etc.
    
    var body: some View {
        List(viewModel.rows) { Row(rowModel: $0) }
    }
}

struct Row : View {
    @ObjectBinding var rowModel: RowModel
    
    var body: some View {
        HStack {
            Text(rowModel.name)
            Spacer()
            Text(rowModel.isTyping ? "Yes" : "No")
        }
    }
}

class ViewModel: BindableObject {
    let didChange = PassthroughSubject<Void, Never>()
    
    init() {
        rows = [RowModel].init(arrayLiteral: RowModel(), RowModel(), RowModel(), RowModel(), RowModel())
        makeABunchOfChanges()
    }
    
    var rows: [RowModel] { didSet { didChange.send(()) } }
    
    private func makeABunchOfChanges() {
        Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { x in
            if self.rows.count < 10 {
                self.rows.insert(RowModel(), at: Int.random(in: 0..<self.rows.count))
            }
        }
        
        Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { x in
            if self.rows.count > 8 {
                self.rows.remove(at: Int.random(in: 0..<self.rows.count))
            }
        }
    }
}

class RowModel: Identifiable, BindableObject {
    let didChange = PassthroughSubject<Void, Never>()
    
    let id: String = UUID().uuidString
    let name: String
    var isTyping: Bool { didSet { didChange.send(()) } }
    private static var nextNumber: Int = 0
    
    init() {
        isTyping = false
        name = "Row \(RowModel.nextNumber)"
        RowModel.nextNumber += 1
        makeABunchOfChanges()
    }
    
    private func makeABunchOfChanges() {
        Timer.scheduledTimer(withTimeInterval: 0.7, repeats: true) { x in
            guard Int.random(in: 0..<3) == 0 else { return }
            self.isTyping = !self.isTyping
        }
    }
}
@pomello
Copy link

pomello commented Apr 6, 2020

it's not correct on ios 13.4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment