Basically I try to figure out when my viewModel get updated, it will notify view and it will refresh whole body. How to avoid that. For example if my view GoLiveView already present another view BroadcasterView, and later my goLiveViewModel get updated, GoLiveView will be refreshed, and it will create BroadcasterView again , because showBroadcasterView = true. And it will cause so many issues down the road, because of that.
struct GoLiveView: View {
@ObservedObject var goLiveViewModel = GoLiveViewModel()
@EnvironmentObject var sessionStore: SessionStore
@State private var showBroadcasterView = false
@State private var showLiveView = false
init() {
goLiveViewModel.refresh()
}
var body: some View {
NavigationView {
List(goLiveViewModel.rooms) { room in // when goLiveViewModed get updated
NavigationLink(destination: LiveView(clientRole: .audience, room: room, showLiveView: $showLiveView))) {
LiveCell(room: room)
}
}.background(Color.white)
.navigationBarTitle("Live", displayMode: .inline)
.navigationBarItems(leading:
Button(action: {
self.showBroadcasterView = true
}, label: {
Image("ic_go_live").renderingMode(.original)
})).frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(red: 34/255, green: 34/255, blue: 34/255))
.sheet(isPresented: $showBroadcasterView) { // here is problem, get called many times, hence reload whole body ,and create new instances of BroadcasterView(). Because showBroadcasterView = is still true.
BroadcasterView(broadcasterViewModel: BroadcasterViewModel(showBroadcasterView: $showBroadcasterView))
.environmentObject(self.sessionStore)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.clear)
}
}
}
this is my GoliveViewModel
typealias RoomsFetchOuput = AnyPublisher<RoomsFetchState, Never>
enum RoomsFetchState: Equatable {
static func == (lhs: RoomsFetchState, rhs: RoomsFetchState) -> Bool {
switch (lhs, rhs) {
case (.loading, .loading): return true
case (.success(let lhsrooms), .success(let rhsrooms)):
return lhsrooms == rhsrooms
case (.noResults, .noResults): return true
case (.failure, .failure): return true
default: return false
}
}
case loading
case success([Room])
case noResults
case failure(Error)
}
class GoLiveViewModel: ObservableObject {
private lazy var webServiceManager = WebServiceManager()
@Published var rooms = [Room]()
private lazy var timer = Timer()
private var cancellables: [AnyCancellable] = []
init() {
timer = Timer.scheduledTimer(timeInterval: 4.0, target: self, selector: #selector(refresh) , userInfo: nil, repeats: true) // call every 4 second refresh
}
func fetch() -> RoomsFetchOuput {
return webServiceManager.fetchAllRooms()
.map ({ result -> RoomsFetchState in
switch result {
case .success([]): return .noResults
case let .success(rooms): return .success(rooms)
case .failure(let error): return .failure(error)
}
})
.eraseToAnyPublisher()
let isLoading: RoomsFetchOuput = .just(.loading)
let initialState: RoomsFetchOuput = .just(.noResults)
let idle: RoomsFetchOuput = Publishers.Merge(isLoading, initialState).eraseToAnyPublisher()
return Publishers.Merge(idle, rooms).removeDuplicates().eraseToAnyPublisher()
}
@objc func refresh() {
cancellables.forEach { $0.cancel() }
cancellables.removeAll()
fetch()
.sink { [weak self] state in
guard let self = self else { return }
switch state {
case let .success(rooms):
self.rooms = rooms
case .failure: print("failure")
// show error alert to user
case .noResults: print("no result")
self.rooms = []
// hide spinner
case .loading: print(".loading")
// show spinner
}
}
.store(in: &cancellables)
}
}
SwfitUI has a pattern for this. It needs to conform custom view to Equatable
protocol
struct CustomView: View, Equatable {
static func == (lhs: CustomView, rhs: CustomView) -> Bool {
// << return yes on view properties which identifies that the
// view is equal and should not be refreshed (ie. `body` is not rebuilt)
}
...
and in place of construction add modifier .equatable()
, like
var body: some View {
CustomView().equatable()
}
yes, new value of CustomView
will be constructed every time as superview refreshing (so don't make init
heavy), but body
will be called only if newly constructed view is not equal of previously constructed
Finally, it is seen that it is very useful to break UI hierarchy to many views, it would allow to optimise refresh a lot (but not only good design, maintainability, reusability, etc. :^) ).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With