ComponentKit and FLUX architecture - avoid unnecessary re-rendering of the UI - ios

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.

Related

ViewModel Live Data observers calling on rotation

In my view model, I have two properties:
private val databaseDao = QuestionDatabase.getDatabase(context).questionDao()
val allQuestions: LiveData<List<Question>> = databaseDao.getAllQuestions()
I have observers set on "allQuestions" in my fragment and I'm noticing the observer is being called when I rotate the device. Even though the View Model is only being created once (can tell via a log statement in init()), the observer methods are still being called.
Why is this? I would think the point is to have persistency in the View Model. Ideally, I want the database questions to be only loaded once, regardless of rotation.
This happens because LiveData is lifecycle aware.
And When you rotate the screen you UI Controller [Activity/Fragment] goes through various lifecycle states and lifecycle callbacks.
And since LiveData is lifecycle aware, it updates the detail accordingly.
I have tried to explain this with following points:
When the UI Controller is offscreen, Live Data performs no updates.
When the UI Controller is back on screen, it gets current data.
(Because of this property you are getting above behavior)
When UI controller is destroyed, it performs cleanup on its own.
When new UI Controller starts observing live data, it gets current data.
add this check inside observer
if(lifecycle.currentState == Lifecycle.State.RESUMED){
//code
}
I have the same issue, after reading the jetpack guideline doc, I solve it. Just like what #SVK mentioned, after the rotation of the screen, activity/fragment were re-created.
Base on the solution https://stackoverflow.com/a/64062616,
class SingleLiveEvent<T> : MutableLiveData<T>() {
val TAG: String = "SingleLiveEvent"
private val mPending = AtomicBoolean(false)
#MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
if (hasActiveObservers()) {
Log.w(TAG, "Multiple observers registered but only one will be notified of changes.")
}
// Observe the internal MutableLiveData
super.observe(owner, Observer<T> { t ->
if (mPending.compareAndSet(true, false)) {
observer.onChanged(t)
}
})
}
override fun observeForever(observer: Observer<in T>) {
if (hasActiveObservers()) {
Log.w(TAG, "Multiple observers registered but only one will be notified of changes.")
}
// Observe the internal MutableLiveData
super.observeForever { t ->
if (mPending.compareAndSet(true, false)) {
observer.onChanged(t)
}
}
}
#MainThread
override fun setValue(#Nullable t: T?) {
mPending.set(true)
super.setValue(t)
}
/**
* Used for cases where T is Void, to make calls cleaner.
*/
#MainThread
fun call() {
value = null
}

Complex SwiftUI view performance

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.

With MVVM and ReactiveCocoa, how to handle the delegate pattern in iOS?

A common situation is to have a View Controller A, and it has some information which will be sent to View Controller B; and B will edit the information, when B finishes editing the information, B will call the delegate method to update A, and pop itself from the navigation controller.
How to handle this problem with MVVM and ReactiveCocoa?
Heavy use of ReactiveCocoa, in general, will start pushing you away from the delegate pattern. However, since much of the code you've already written and all of the code you'll encounter in the iOS standard libraries use it, being able to interact with it is still important.
You'll want to use the -[NSObject rac_signalForSelector:] category, that will return a signal that receives a RACTuple value of the arguments to a method each time it is invoked, and completes when the object sending the signal is deallocated.
Let's say you have a UIViewController to display that contains a list of checkboxes a user can select, with a continue button at the bottom. Since the selections change over time, you could represent it as an RACSignal of NSIndexSet values. For the purposes of this example, let's say you must use this class as is, and it currently declares a delegate pattern that contains the following:
#class BSSelectionListViewController;
#protocol BSSelectionListViewControllerDelegate <NSObject>
- (void)listChangedSelections:(BSSelectionListViewController*)list;
- (void)listContinueTouched:(BSSelectionListViewController*)list;
#end
When you present the view controller from elsewhere (like a UIViewController at the top of the navigation stack), you'll create the view controller and assign self as the delegate. It might look something like
BSSelectionListViewController* listVC = [[BSSelectionListViewController alloc] initWithQuestion:question listChoices:choices selections:idxSet];
listVC.delegate = self;
[self.navigationController pushViewController:listVC];
Before pushing this UIViewController on the stack, you'll want to create signals for the delegate methods that it could call:
RACSignal* continueTouched = [[[self rac_signalForSelector:#selector(listContinueTouched:)]
takeUntil:list.rac_willDeallocSignal]
filter:^BOOL(RACTuple* vcTuple)
{
return vcTuple.first == listVC;
}];
RACSignal* selections = [[[[self rac_signalForSelector:#selector(listChangedSelections:)]
takeUntil:list.rac_willDeallocSignal]
filter:^BOOL(RACTuple* vcTuple)
{
return vcTuple.first == listVC;
}]
map:^id(RACTuple* vcTuple)
{
return [vcTuple.first selections];
}];
You can then subscribe to these signals to do whatever side effects you need. Maybe something like:
RAC(self, firstChoiceSelected) = [selections map:^id(NSIndexSet* selections)
{
return #([selections containsIndex:0]);
}];
and
#weakify(self)
[continueTouched subscribeNext:^(id x)
{
#strongify(self)
[self.navigationController popToViewController:self];
}];
Because it's possible that you might have several of these screens that you're the delegate of, you want to make sure that you are filtering down to just this one in your RACSignals.
ReactiveCocoa will actually implement these methods (the ones in the delegate protocol) for you. However, to keep the compiler happy, you should add stubs.
- (void)listChangedSelections:(BSSelectionListViewController *)list {}
- (void)listContinueTouched:(BSSelectionListViewController*)list {}
This is, IMO, an improvement over the standard delegate pattern, where you would need to declare an instance variable to hold the selection view controller, and check in the delegate methods which controller is calling you. ReactiveCocoa's rac_signalForSelector method can reduce the scope of that state (this view controller comes and goes over time) in to a local variable instead of an instance variable. It also allows you to be explicit about dealing with the changes to the selections.

Why Scene(Action-Set) added to Home(HMHome) always have updated Characteristic's(HMCharacteristic) values :HomeKit

I am working on a Demo iOS application based on HomeKit API's.
I have created Scene(Actions-Set) AS1 for Particular Home(H1) with some services(S1 S2...) to perform multiple action in a go.
I can create multiple scene without any problems, but I am facing problem in updating the any of created scene
Flow of My Application
Show Added Home(ListView)
Click on Any Home, detail screen appears With few Options(Accessory A1 ,Room A2 ,Scene A3....)
Clicking on A1 Add the Accessory(can change the characteristic of added Accessory's service from here)
A3 has a list of added Scene (Action-Set) as well a button to add new Scene (Action-Set) to current Home
User can Click on any added Scene(ActionSet) to update its actions and name both
Problem : Once I changed the characteristic of services from Accessory A1 option then characteristics added to Scene's action gets updated to the same value.
My Assumption: I was thinking each created Scene (Action-Set) maintain its own characteristics's value separately and if user modify characteristics of any service from accessory A1(from somewhere else) then it should not affect the value of actions added to saved Scene(pre-condition - user picked that accessory in created Scene's action)
My Approach to Update Action-Set(Scene) as,
Once user click on any added Action-Set services list appear where user can click on any services to updated its characteristic
Access Actions of current Action-Set
Access characteristic of each action( HMCharacteristicWriteAction)
Access service of characteristic
Create instance of CustomServices(to keep track of some other info like user included that service to into current action set etc.) and add it to Data-source and avoid repetition
Once user done with value change and click on update actions button
Start the Update process
Update Scene (action-set)
1.Check if User changed the name of Action-set,
Then update the name of action set first, once success block executed delete all past added actions if exist.
Add new actions to updated scene.
2.If Scene(action-set) name is same as Old Name,
Delete all previous actions if exist.
Add new actions to updated scene.
Here is the code snippet(prepare datasource) which is use to populate services listView
func prepareDataSource(){
var actionsArray = NSArray(array:
self.currentScene!.actions.allObjects)
for (var index = 0 ; index<actionsArray.count; index++ ){
var sWritttenAction:HMCharacteristicWriteAction? = actionsArray[index] as? HMCharacteristicWriteAction
if let sActionCharacteristic = sWritttenAction!.characteristic{
var accessoryService = sActionCharacteristic.service as HMService
if(!isServiceAlraedyAdded(accessoryService.name)){
var sceneService = DMIAccessoryService.initWithService(accessoryService)
sceneService.isSelected = true
//Add Services
self.services.append(sceneService)
self.addedServices.append(sceneService)
println("its a diffrent service")
}
else
{
println("Already has the same service")
}
}
}
}
I have gone through the HomeKit documentation many times but found nothing related to this issue.
As far as I understand the action-set's concept it should maintained characteristics value separately so that we could change it's actions values later on.
I have been facing the this problem since last couple of days(because couple of days back posted the same problem here) and Now I am sure it is a bug in apple home api because once I try to re-execute the same action set( pre-condition: services characteristic's value changed by user from A1 option).It is executed successfully and update the all those accessories which have the same characteristics similar to created Scene (ActionSet).
Please anybody who has found similar issue with update Scene(ActionSet) help me out.
If my approach is wrong, Please feel free to point me.
Scene can be created with HMActionSet object, It expose only 3 apis i.e updateName(), addAction(), removeAction()
updateName can be used if user want to change name of scene. It should not call any other api (to removeAction or addAction)
Add Action
addAction () is used to change the behavior of any Accessory using scene by using HMCharacteristic of Accessory. That characteristic must support WriteAction that can be check by.
[characteristic.properties containsObject:HMCharacteristicPropertyWritable]
How to load characteristic
Characteristic can be displayed in Tableview or custom view can be created. And the value of that characteristic can be set to displayed view.
If current charcateristic is already added to actionset, we need to fetch the value from actionset objects's actions property.
- (void) readValueForCharacteristic:(HMCharacteristic *)characteristic completion:(void (^)(id, NSError *))completion {
for (HMCharacteristicWriteAction *action in self.actionSet.actions) {
if ([characteristic isEqual:action.characteristic]) {
completion([self getTargetValueForCharacteristic:characteristic],nil);
return;
}
}
[characteristic readValueWithCompletionHandler:^(NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
completion(characteristic.value, error);
});
}];
}
-(id) getTargetValueForCharacteristic:(HMCharacteristic *)characteristic {
id value;
for (HMCharacteristicWriteAction *action in self.actionSet.actions) {
if ([action.characteristic isEqual:characteristic]) {
value = action.targetValue;
}
}
return value;
}
Now this value can be set on both while adding characteristic to scene or updating the characteristic.
As it checks that action is added to Scene ? then it will get value from actionSet. Else it will read current value of that chracteristic.
Saving scene
Same while adding Scene or updating scene. Just need to add a condition.
/** Create property */
#property (nonatomic) dispatch_group_t sceneGroup;
#property (nonatomic) NSError *error;
/** Initialize */
self.sceneGroup = dispatch_group_create();
- (void)saveActionSetWithName:(NSString *)name
completionHandler:(void (^)(NSError *error))completion {
if (self.actionSet) {
[self saveScene:self.actionSet];
[self updateNameIfRequire:name];
} else {
[self createScene:name];
}
dispatch_group_notify(self.sceneGroup, dispatch_get_main_queue(), ^{
completion(self.error);
self.error = nil;
});
}
If scene is already created then add Writable actions to actionset in saveScene: method. Also check if name of scene is changed and just change name using updateName: method if require.
If scene is not created, Create scene using HMHome class's method addActionSetWithName: by using home object, on its completion block call saveScene method and in that add writable actions to created scene.
You can take referance of HMCatalog sample project by apple.
Its good if you create model custom class to manage Actionset.
Ask me if you have any query.

Monotouch.Dialog - How to push a view from an Element

It seems like this should be very easy, but I'm missing something. I have a custom Element:
public class PostSummaryElement:StyledMultilineElement,IElementSizing
When the element's accessory is clicked on, I want to push a view onto the stack. I.e. something like this:
this.AccessoryTapped += () => {
Console.WriteLine ("Tapped");
if (MyParent != null) {
MyParent.PresentViewController(new MyDemoController("Details"),false,null);
}
};
Where MyDemoController's gui is created with monotouch.dialog.
I'm just trying to break up the gui into Views and Controlls, where a control can push a view onto the stack, wiat for something to happen, and then the user navigates back to the previous view wich contains the control.
Any thought?
Thanks.
I'd recommend you not to hardcode behavior in AccessoryTapped method, because the day when you'll want to use that component in another place of your project is very close. And probably in nearest future you'll need some another behavior or for example it will be another project without MyDemoController at all.
So I propose you to create the following property:
public Action accessoryTapped;
in your element and its view, and then modify your AccessoryTapped is that way:
this.AccessoryTapped += () => {
Console.WriteLine ("Tapped");
if (accessoryTapped != null) {
accessoryTapped();
}
};
So you'll need to create PostSummaryElement objects in following way:
var myElement = new PostSummaryElement() {
accessoryTapped = someFunction,
}
...
void someFunction()
{
NavigationController.PushViewController (new MyDemoController("Details"), true);
}

Resources