Create recursive layout - ios

I'm creating an application that receives a dynamic and recursive data structure, ie, an element can have an identical element within it, something similar to the HTML´s DOM.
At this stage I have "simple" elements type text, date, buttons, and others likes: grid, tables and sections. These last ones, can have a elements inside is structure, something like childrens, for example, a grid element can have other grid.
These models elements are already being built on a framework through my JSON received, usign a Factory pattern. In that order, i have already a list of elements to be drawn in the view.
My first approach was to implement a UICollectionView, but now i am having trouble drawing elements belongings to others.
Any idea what the best approach to solve this problem? always taking into account the performance of the app.
Thanks in advance

Beware of auto layout in such complex layouts. Auto layout has a cost, and the more views and more complex the scene is, the more toll it takes. Adding and removing constraints is costly, and layout passes themselves are costly.
Looks to me a rather simple layout where a collection view is placed inside a table view. Main view seems like a table view (can be a collection but is simple for that). Each element is a cell. Since you tell the table view the cell size, you can have whatever you want inside as long as you can calculate the size. In fact, the section element to me looks like the same table as the main table. Grid element is a collection view. You could do it as a grid view, and then you can use more reuse.
So let's take a look. Simple elements first, text element, date element, title section and footer section are either simple table view cells or collection view cells. Table element looks like a collection view to me but can be anything custom. A grid is a collection view with a flow layout. If you implement everything with collections, you can reuse the same classes for text and date elements as the ones in the main view, otherwise you use collection view cells here. Section element is a cell with a table view. The cell can act as a data source of the table view, or there can be a "nested table controller" which the cell can use as data source of the table view. To get the height of the section element cell, you can get the table view's content size's height and return that. If we further consider, the main view is basically a top level section element table. If you pick the "nested table controller" root, your main view controller can have that as a datasource of the main table view.
To track elements in the section nested view controller (or cell-as-data-source model), I recommend having an array of data representing, and an enum to determine the type. This way you can design the various types and select the cell identifiers based on the enum.
A crude object representation of this backing data structure would look like this:
{
type: Section
items: {
{
type: Text
items: {}
},
{
type: Date
items: {}
},
{
type: Grid
items: {
{
type: Text
items: {}
},
{
type: Date
items: {}
},
{
type: Text
items: {}
},
{
type: Text
items: {}
},
{
type: Date
items: {}
}
}
},
{
type: Section
items: {
{
type: Title
items: {}
},
{
type: Text
items: {}
},
{
type: Grid
items: {
{
type: Text
items: {}
},
{
type: Text
items: {}
},
{
type: Text
items: {}
}
}
}
{
type: Text
items: {}
},
{
type: Footer
items: {}
},
}
},
{
type: Table
items: {
...
}
}
}
}
This gives you the same example as your above example scheme.
Parsing this is very simple. At the top, you can either always assume there is a section element, or, even better, just iterate the top level items and use each item's type and in cellForRow: return an identifier according to the type, and if a complex type, pass the array of sub-items to the cell, who would perform the same logic and so on.
I used a table view for the vertical flow because they are easier to manager. You can use a collection view with a vertical flow layout as well.
Using iOS7's table view estimatedRowHeight, you could even not parse everything but only the most immediate cells for optimization, and create more and more as the user scrolls.

I am not 100% sure about your questions but sticking to your 'how can I create dynamic layout supporting recursion', I'd advise the following
Create a UIView subclass
Give it a model property (e.g. #property GridModel model) or give it an initialiser method with model (e.g initWithModel:(GridModel *)model)
In either drawRect method or initialiser methods, make it draw its subviews based on the model
In your code, parse the json and according to the type of object, initialise this special subview and make it draw
To draw many subviews dynamically and recursively, keep track of your y offset and make sure next subview starts from where the previous subview ends
Does this make sense?

Related

jetpack compose lazy custom layout

I tried to develop a simple custom layout just like the documentation
#Composable
fun MyBasicColumn(
modifier: Modifier = Modifier,
content: #Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
// Don't constrain child views further, measure them with given constraints
// List of measured children
val placeables = measurables.map { measurable ->
// Measure each children
measurable.measure(constraints)
}
// Set the size of the layout as big as it can
layout(constraints.maxWidth, constraints.maxHeight) {
// Track the y co-ord we have placed children up to
var yPosition = 0
// Place children in the parent layout
placeables.forEach { placeable ->
// Position item on the screen
placeable.placeRelative(x = 0, y = yPosition)
// Record the y co-ord placed up to
yPosition += placeable.height
}
}
}
}
it works fine when I know exact number of items
but what about lazy items?
there is nothing in documentation about how can I develop a LazyCustomLayout
You don't exactly have to know how many items are in the Layout, since even for dynamic lists, there's always a 'current number of items' which can be computed. Let's say you download a list of texts from a server, and then intend to use this Layout to render those. Even in that case, while the server may vary the length of the list, i.e., the list is dynamic in size, you would presumably have a LiveData object keeping track of the list items. From there, you can easily use the collectAsState() method inside a Composable, or the observeAsState() method tied to a LifecycleOwner to convert it into the Compose-compatible MutableState<T> variable. Hence, whenever the LiveData notifies a new value (addition, or deletion), the MutableState<T> variable will also be updated to reflect those values. This, you can use inside the said Layout, which is also a Composable and hence, will update along-side the server-values, in real-time.
The thing is, no matter how you get your list, in order to show it on-screen, or use it anywhere in your app, you would always have a list object, which can be exploited using Compose's declarative and reactive nature.

Show entire table view inside cell

I need to display a dynamic table view inside a cell (of a static table). Using sections instead will not be enough for me. But I don't want this table to be scrollable, so the entire table must appear at once. The problem is that this table size [and rows count, and each row size] varies according to the content being shown. How can I associate the cell (which holds the table) autoresizing property with a table inside that must show all content at once?
Currently I have the tableView inside the cell, and constraints bonds it to all the 4 sides. The first table (not the one inside the cell) rowHeight property is set to UITableViewAutomaticDimension, but the table inside the cell doesn't appear entirely.
If I set the static cell height to a value greater than the tableView(inside cell) height, the whole table appears, and also an extra space beneath it (as the table is bounded to 4 sides of the cell)
So, any ideas on how to show this entire table inside a cell that dynamically has the perfect size for it ?
ps: I tried using collection view inside the cell. Unfortunately it doesn't serve my purpose.
Thanks!
Update
I tried to create a class for the inner table and use (as pointed by iamirzhan) contentSize didSet, like so:
override var contentSize:CGSize {
didSet {
self.invalidateIntrinsicContentSize()
}
}
I then used this method to call a function that resizes the cell that holds the table: self.frame.size.height = table.contentSize.height. The function is on this cell's own class.
This worked, the table now appears entirely. The problem is that it overlaps the cell underneath it, so i'm still looking for a solution.
I don't see why this requires 2 tableviews. The static portion should be uitableviewheaderfooters or just a UIStackView. But to answer your question simply query your child tableView for its size and return this in tableView:heightForRowAtIndexPath: for the outer tableview. The height of the child tableView is simply the sum of the hieght of all of its children (including any headers/footers). This is usually not difficult to calculate unless you are using something like a webview where you need to actually load the content and get the size asynchronously. You can calculate the size of elements that are based on their intrinsic content size with UIView.sizeThatFits(_:). The other elements should have fixed sizes or constants in the storyboard that you can sum up.
For inner tableView you should write such implementation.
Firstly, disable the scrollEnable flag
Then you should override the intrinsicContentSize method of tableView
Your inner tableView custom class:
class IntrinsicTableView: UITableView {
override var contentSize:CGSize {
didSet {
self.invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
self.layoutIfNeeded()
return self.contentSize
}
}
Now, delete the height constraint, and the height will be calculating by its content.

ui-grid - How to pin a ROW to the top

I'm using the angularjs ui-grid and I have a "total" row that I want to pin to the top of the grid no matter what is the current sorting.
How can I accomplish that?
I think this is what you are looking for : Row Pinning
Essentially add another hidden column, something like this:
{
field: 'pinned',
visible: false,
sort: {direction: uiGridConstants.ASC, priority: 0}, //use uiGridConstants.DESC for pinning to the bottom
suppressRemoveSort: true,
sortDirectionCycle: [uiGridConstants.ASC] //use uiGridConstants.DESC for pinning to the bottom
}
Row entities which have pinned = true rise to the top, even when other sorting are applied.
DISCLAIMER: I know it's not exactly answers the question, but this is how I solved it for now until I'll have a better solution:
Create an other grid above the main grid :
<div style="height:30px" ui-grid="totalGridOptions"></div>
<div ui-grid="gridOptions" class="grid"></div>
with definitions:
$scope.totalGridOptions = {showHeader:false,enableHorizontalScrollbar:false};
and then bind the columns of the main grid to the total grid (for width and other adjustments):
$scope.$watch('gridOptions', function (newVal) {
$scope.totalGridOptions.columnDefs = newVal.columnDefs;
}, true);
I think you should use something like this
$scope.gridOptions.data.unshift({label:value});
unshift adds it to the top
Edit 2 / Actual Solution: The way I finally settled this issue was by using custom header cell templates. I essentially create a second header row by adding a div at the bottom of what was previously my header. Here's a simple version:
<div class="super-special-header>
<div class="header-display-name">{{col.displayName}}</div>
<div class="totals-row">{{grid.appScope.totals[col.field]}}</div>
</div>
I store my totals on the controller's $scope and can access them in that div with grid.appScope.totals[whateverThisColumnIs]. This way I can still update them dynamically, but they don't get mixed into a sort function like my previous 'solution' was aiming for.
Edit 1 / Dead-end 'solution': Just ran into a problem with my solution, if your table is long (you have to scroll to get to bottom rows), the totals row will scroll out of view. Going to leave this 'solution' here so no one else makes the same mistake!
I had this same issue but with a twist. Since I was going to need to change the default sorting algorithms for many of the columns anyway, I set my algorithm up to skip the first element in the sort. You can use the sortingAlgorithm property on any columndef that would be part of a sortable column. This is really only a solution if you have only a few sortable columns though. It becomes unmaintainable for huge tables.
I couldn't find any built-in feature for ui-grid to keep a specific row at the top of the grid when sorting is applied. But this could be done using sortingAlgorithm parameter in the columnDefs( please refer to http://ui-grid.info/docs/#!/tutorial/Tutorial:%20102%20Sorting).
I have written an algorithm which keeps the row('total' is the particular cell value in the row) at the top of the grid without applying a sorting.
var sortingAlgorithm = function (a, b, rowA, rowB, direction) {
if (direction == 'total') {
if (a == 'total') {
return 0;
}
return (a < b) ? -1 : 1;
} else {
if (a == 'total') {
return 0;
}
if (b == 'total') {
return 1;
}
}
}

add textfield on clicking a button in sproutcore

How to add more textfields in a view on clicking a button in the same view in sproutcore?
I have a sliding pane with particular number of textfields. On clicking a button, I need to add more number of text field in the same view.
Or,
I should be able to select the number from a select button view and show those many number of textfield in the same view.
I would recommend using a SC.ListView for this purpose.
You should have a SC.ArrayController whose content is an array containing objects that represent each text field. This may be as simple as something like this:
MyApp.myController = SC.ArrayController.create({
content: [
SC.Object.create({ someProperty: "Text field value 1" }),
SC.Object.create({ someProperty: "Text field value 2" }),
SC.Object.create({ someProperty: "Text field value 3" })
]
});
Next, you'll create your SC.ListView and bind it's content to the controller, and create the exampleView whose content is bound to the someProperty property of the objects:
MyApp.MyView = SC.View.extend({
childViews: 'scrollView addButtonView'.w(),
scrollView: SC.ScrollView.extend({
layout: { top: 0, left: 0, right: 0, bottom: 50 },
contentView: SC.ListView.extend({
contentBinding: 'MyApp.myController.arrangedObjects',
rowHeight: 40,
exampleView: SC.View.extend({
childViews: 'textFieldView'.w(),
textFieldView: SC.TextFieldView.extend({
// Add a little margin so it looks nice
layout: { left: 5, top: 5, right: 5, bottom: 5 },
valueBinding: 'parentView.content.someProperty'
})
})
})
}),
addButtonView: SC.ButtonView.extend({
layout: { centerX: 0, bottom: 10, width: 125, height: 24 },
title: "Add Text Field",
// NOTE: The following really should be handled by a statechart
// action; I have done it inline for simplicity.
action: function() {
MyApp.myController.pushObject(SC.Object.create({ value: "New Field" }));
}
})
});
Now, when you click the "Add Text Field" button, it will add a new object to the controller array which will automatically re-render the list view with the new object and hence, the new text field.
A couple of notes:
This uses a SC.ScrollView in conjunction with the SC.ListView, you will almost always want to do it this way.
Since we are using standard bindings (not SC.Binding.oneWay()), editing the text fields will automatically update the someProperty property in the objects in MyApp.myController and vice versa: if you update the value via some other means, the text field should automatically update as well.
This should not be used for a large list as using the childViews method of view layout can be slow. If you need performance, you should change the exampleView to a view that overrides the render() method and manually renders a text input and sets up the proper change events and bindings.
Lastly, I can't remember if the proper syntax for the text field's valueBinding is parentView.content.someProperty or .parentView.content.someProperty (notice the period at the beginning). If the first way doesn't work, try adding the . and see if that works.
Like Topher I'm assuming you're using SproutCore not Ember (formerly SC2).
If you need to add an arbitrary child view to an arbitrary location on your view, you want view.appendChild. In the button's action, you would do something like this:
this.get('parentView').appendChild(SC.View.create({ ... }))
If you go this route, you'll have to figure out the layout for the new view yourself. If you don't need to precisely control the layout, then go with Topher's solution - the ListView does the layout piece for you.

Implement the Android Market layout in an iOS view

I'm looking for an efficient way to implement the same kind of layout that the Android market.
Right now I'm using a UITableView with custom cells. The data I'm sending from the server is something of the sort:
[
{
url: "app://url_of_content_in_the_app",
image: "http://url of the featured image",
height: 100
}
},
{
url: "app://url_of_content_in_the_app",
image: "http://url of the featured image",
height: 100
}
]
And I was thinking of updating the custom cell to support 2 or 3 buttons inside each cell, but I'm not sure this is the most efficient way to handle this. I was also thinking about a simple HTML page but I want the content to be cacheable easily.
One last point, the UITableView way only handles horizontal subdivisions such as:
_______
|__|__|
|_____|
I can't do something like:
_______
| |__|
|__|__|
You can do something like your last example (three-way cell), just have a cell two units high that you present three cells in as separate views.
Or at some point you may just decide putting views into a scroll view makes more sense, if there are not enough aspects of a UITableView you are making use of.
I would suggest putting two (or 3) custom subclasses of UIView inside the cell.
If you want them be be user interact-able - add your own UIGestureRecognizer to the view.
NSRect b = cell.contentView.bounds;
UIView *a = [[MyView alloc] initWithFrame:NSRectMake(NSRectGetMinX(b),NSRectGetMinY(b), NSRectGetWidth(b)/2., NSRectGetHeight(b))];
UIView *b; //... similarly
// set auto resize mask for a/b appropriately
[cell.contentView addSubview:a];
[cell.contentView addSubview:b];
(not checked for typos)
having a custom cell with that initialization code within it is a good idea

Resources