I am trying to get the maximum performance out of SwiftUI. I have a complex view and a lot of state and not sure what penalties I am paying as I calculate a lot of views programatically.
The view is something like this, stripped to the bare minimum
struct ComplexView : View {
#State var a = 1
#State var b = 2
// .. more state
// Perform function and change some of the state
func doIt ( index : Int ) {
// ... change some state
}
// Calculate some view based on state and index
func calcView ( index : Int ) -> some View {
// Debug
print ( "Generating view for index \( index )" )
// e.g. based on state and index and also use doIt function
// note this requires access to this struct's functionality
return Button ( action : { self.doIt ( index : index ) },
label : // ... complex calc based on index and maybe on state
)
}
var body : some View {
// ... lots of complex views based on state
DirtyA
DirtyB
// ... lots of complex views NOT based on state
CleanA
CleanB
// ... lots of calculated views that are based on state
calcView( index : 1 ) // dirty
calcView( index : 2 ) // dirty
// ... lots of calculated views that are NOT based on state
calcView( index : 101 ) // clean
calcView( index : 102 ) // clean
}
}
My question really is how to do the body for maximum performance. At present it seems everything gets rerendered everytime state changes. How do I prevent that?
Should I put all 'dirty' views in their own struct bound to state so that only they are rerendered?
Should I put all 'clean' views in their own struct to prevent those from rerendering?
SwiftUI is supposed to be clever enough to render only changes - can I safely trust this?
That is the easy part, how about the calculated ones? How do I ensure the dirty ones are rerendered when state changes without rerendering the clean ones?
FWIW the one debug line in the code above that prints when a view is calculated is what leads me to this question. It prints everytime and for all indices when state changes, so I think there is a lot of unnecessary rendering taking place especially for the calculated views. However, if SwiftUI is clever enough to only rerender changes, then I need not worry and need not change my approach.
Related
I am using SwiftUI to create an infinite scrolling List by:
// My data structure
struct MyData {
title: String
}
// My datasource variable
#State var datasource: [MyData]
// My List view
List(datasource.enumerated().map({ $0 }), id: \.title) { (index, myData) in
MyListRow(myData)
.onAppear(perform: {
if index == datasource.count - 2 {
fetchMoreData()
}
})
}
By this code, when I scroll to the bottom, the List automatically increases as I wish.
However, in fact, the WHOLE List was redrawn! Which means the List rows that has been previously constructed were de-inited and inited again.
My question is: How to prevent SwiftUI from redrawing the whole list, but only draw the newly added rows?
Inited is not drawn. By modifying datasource you initiated List refresh, which resulted in reconstructing row values, but redrawn (called body) only for those which differ from previous. Moreover List caches rows and does not keep more than fit onto screen, plus couple, at all. So it just cannot redraw whole bunch of rows, because ignores invisibles.
So, just don't do anything heavy in View.init, which I assume you do and complain on it's re-doing on refresh.
I have a SwiftUI View where I declare a condition like this
#State var condition = UserDefaults.standard.bool(forKey: "JustKey")
When I first push this view into the navigation stack condition variable is getting the correct value. Then when I pop this View I change the value in UserDefaults but when I push this screen again condition variable remembers the old value which it got first time.
How to find a workaround for this because I want to reinitialize my condition variable each time I enter my custom view where I declared it?
In this case, #State is behaving exactly like it is supposed to: as a persistent store for the component it's attached to.
Fundamentally, pushing a view with a NavigationLink is like any other component in the view hierarchy. Whether or not the screen is actually visible is an implementation detail. While SwiftUI is not actually rendering your hidden UI elements after closing a screen, it does hold on to the View tree.
You can force a view to be completely thrown away with the .id(_:) modifier, for example:
struct ContentView: View {
#State var i = 0
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: DetailView().id(i)) {
Text("Show Detail View")
}
Button("Toggle") {
self.i += 1
UserDefaults.standard.set(
!UserDefaults.standard.bool(forKey: "JustKey"),
forKey: "JustKey")
}
}
}
}
}
The toggle button both modifies the value for JustKey and increments the value that we pass to .id(i). When the singular argument to id(_:) changes, the id modifier tells SwiftUI that this is a different view, so when SwiftUI runs it's diffing algorithm it throws away the old one and creates a new one (with new #State variables). Read more about id here.
The explanation above provides a workaround, but it's not a good solution, IMO. A much better solution is to use an ObservableObject. It looks like this:
#ObservedObject condition = KeyPathObserver(\.JustKey, on: UserDefaults.standard)
You can find the code that implements the KeyPathObserver and a full working Playground example at this SO answer
I'm quite new to FRP and decided to get started with Bond and ReactiveKit as it seemed lightweight enough to start gradually applying it to my apps and my head.
I have a setup where I have a view, which has an observable State and I have a view model with another observable property of type State. I want my view to be dull and unaware of the semantics, so I want my view model to validate the state, transform it and send back to the view.
The view contains a couple of text fields and a segmented control. When the segmented control is 0, I want only the first text field to be visible, otherwise — both fields. Whenever the user enters something or taps the segmented control, the observable state object is updated. Here is the State struct:
enum ValueType {
case text
case number
}
struct State {
var name: String?
var unit: String?
var valueType: ValueType = .text
var showsUnitTextField: Bool = true
}
Here is the best I could come up with:
View model:
override init() {
super.init()
self.bind()
}
let inputState: Observable<State> = Observable<State>(State())
var outputState: Observable<State> = Observable<State>(State())
private func bind() {
inputState.map(self.sanitizeState(_:)).bind(to: outputState)
}
private func sanitizeState(_ state: State) -> State {
var newState = state
newState.showsUnitTextField = state.valueType == .number
return newState
}
View controller:
private func bind() {
myView.reactive.state.bind(to: viewModel.inputState)
viewModel.outputState.bind(to: myView.reactive.state)
}
Basically I'm having two observables, one receives the updates and the other one sends the transformed value back to the view. This solutions seems to introduce a heavy boilerplate and I'm looking for a better one. Does anyone have any idea?
P.S. Other scenarios where this could come handy is when I want to have some constraints on the user input (only letters or only digits), or maybe I want to format the input in a fancy way.
Lets say that I've a ScrollableView with 3 Views (forms), those form views have at least 10 fields, take a look at this exemple.
index.js
$.content.add(Alloy.createController('scrollable').getView());
scrollable.js
$.scrollableView.addView(Alloy.createController('form',{
fields:[
{label:'field 1',type:'text'},
{label:'field 1',type:'date',value:'2016-06-08'},
...
]
}).getView());
$.scrollableView.cleanup = function() {
$.destroy();
$.off();
for(var i = parseInt($.scrollableView.views.length); i > 0; i--) if($.scrollableView.views[i-1]) {
if($.scrollableView.views[i-1].cleanup) $.scrollableView.views[i-1].cleanup();
$.scrollableView.views[i-1] = null;
$.scrollableView.removeView($.scrollableView.views[i-1]);
}
$ = args = null;
};
form.js
for(var i in args.fields) $.form.add(Alloy.createController('field',args.fields[i]).getView());
$.form.cleanup = function() {
$.destroy();
$.off();
for(var i in $.form.children) {
if($.form.children[i].cleanup) $.form.children[i].cleanup();
$.form.children[i] = null;
}
$.form.removeAllChildren();
$ = args = null;
};
When I'm cleaning up all the controllers, I still don't understand what it's necessary to do.
When I want to remove the ScrollableView, I run the cleanup function on every View, and it's children.
Should I run the cleanup function on all the ScrollableView views?
Should I null all the ScrollableView views?
Should I remove all the ScrollableView views?
Should I run the cleanup function on all the View children?
Should I null all the View children?
Should I remove all the View children?
UPDATE
In this case, I still need to cleanup all the fields? or setting the data to null will solve that?
form.js
var args = arguments[0],
data = {
fields:{}
};
for(var i in args.fields) {
data.fields[args.fields[i].label] = Alloy.createController('field',args.fields[i]).getView();
$.form.add(data.fields[args.fields[i].label]);
}
$.form.cleanup = function() {
$.destroy();
$.off();
//this is needed?
for(var i in data.fields) {
if(data.fields[i].cleanup) data.fields[i].cleanup();
data.fields[i] = null;
}
//this is needed?
$ = data = args = null;
};
Anyway, if my fields have an event listener added like 'change' or 'click', I must remove it in cleanup function, right?
There is no need to remove all views, the only thing you need to do to clean up memory is remove the most parent view, and all references to anything within the most parent view & the reference to the parent view.
So in your case, you only have to remove the ScrollableView and within the scrollableview you need to do $.off(). $.destroy() is only needed if you use data-binding (models/collections).
Because your child views never have a reference (variable), there is no need to remove them. It is automatically handled by Appcelerator/JavaScript and will be cleaned up with garbage collection when the time comes.
note: Garbage collection doesn't happen directly after you remove the views, so you might still have increased memory usage. Both JavaScript and the native platform have their own garbage collection.
You can read more about memory management in this article on TiDev which is still very relevant.
In your updated question you set all the sub-views in the data object. nulling the data object will also drop all references to the views, so that should be enough.
I'm using Facebook ComponentsKit to generate my views.
I now am migrating to a "flux" architecture for changing app state and triggering view updates.
The main problem I have is that not all state changes should trigger a UI update. I don't understand the "generic" mechanism to avoid that.
Basically the app state is a "big" "JSON" representing a "view model" (Parsed to native typed objects). The JSON contains all of the view declarations and their initial values. (The JSON is quite complex)
For example a simplified JSON representing a view hierarchy containing a "pager" component and a navigation "next" button :
{
... views ...
{
"pager" : {
"id" : "pager-id-xxx",
"currentPage" : 0,
"pages" : [
....pages....
{},
{},
....pages....
]
},
...
"navigation-next-button" : {
"id" : "navigation-next-button-id-xxxx",
"target" : "pager-id-xxx"
}
},
... views ...
}
My "Flux" abstraction looks like:
// "Action" portion
#interface ChangePageAction
#property id destinationId; // very simplified action. wraps the destination "id"
#end
#implementation ChangePageReducer
-(JSON)reduce:(JSON)initialJSON action:(ChangePageAction *)changePageAction {
// the "reduce" portion finds the node of the pager (in the JSON) and changes the value by +1
// something like:
// find the node in the JSON with the changePageAction.destinationID
Node *nodeCopy = getNodeCopy(initialJSON,changePageAction.destinationId);
replaceValue(nodeCopy,nodeCopy.currentPage + 1);
// according to FLUX doctrine we are supposed to return a new object
return jsonCopyByReplacingNode(nodeCopy); // a new JSON with the updated values
}
// the navigation button triggers the new state
#implementation NavigationNextButton {
id _destination; // the target of this action
FluxStore _store; // the application flux store
}
-(void)buttonPressed {
ChangePageAction *changePage = ...
[_store dispatch:changePage];
}
#end
In my "view controller" I now get a "update state" callback
#implementation ViewController
-(void)newState:(JSON)newJSON {
// here all of the view is re-rendered
[self render:JSON];
//The issue is that I don't need or want to re-render for every action that is performed on the state.
// many states don't evaluate to a UI update
// how should I manage that?
}
#end
Unfortunately, there's no easy way to do this in ComponentKit. React has shouldComponentUpdate, but ComponentKit does not have an equivalent.
The good news is that ComponentKit should be smart enough to rebuild all the components, then realize that nothing has actually changed, and end up making no UIKit changes.
The bad news is that it'll spend a fair amount of CPU doing that.