This is an extension of my previous question (Get width of a view using in SwiftUI)
I need to implement a layout where number of items per row is determined dynamically, based on their combined width (basically, place items in a row until they no longer fit).
I've been told that using the GeometryReader is a hacky way to do something in a declarative language, which is obviously true.
I have also been directed to this CollectionView-like component https://github.com/Q-Mobile/QGrid but the solution is static as the number of rows and cells per row is determined once, before any components are rendered.
I have no idea how to approach this, so any advice is very valuable for me!
❤️❤️❤️
TL;DR
GeometryReader may be a "hacky" solution, but it is the solution we have at the moment. It is possible to create a solution that reflows a small number of items dynamically, or a large number of items with a delay. My demo code would be unwieldy here, but it sounds like describing my approach may be useful.
Working with what we've got
Behind the scenes, SwiftUI is doing all kinds of optimized constraint solving to layout your views efficiently. In theory, reflowing content like you describe could be part of that constraint solving; in today's SwiftUI, it is not. Therefore, the only way to do what you are describing is some variant of the following:
Let SwiftUI lay everything out based on our data model.
Get the widths that SwiftUI decided on using Geometry reader and preferences/callbacks.
Use these widths to solve our reflow constraints.
Update the data model, which will trigger step 1.
Hopefully, this process converges to a stable layout, rather than entering an endless loop.
My results
After playing around with it, here's what I've gotten so far. You can see that a small number of items (29 in my example) reflow almost instantaneously as the width is changed. With a large number of items (262 in my example), there is a noticable delay. This shouldn't be much of an issue if the content and view width don't change and won't need to be updated frequently. The time is spent almost entirely in step 1, so until we get proper reflow support in SwiftUI, I suspect this is as good as it gets. (In case you're wondering, the vertical scrollview scrolls with normal responsiveness once the reflow is finished.)
My strategy
Essentially, my data model starts with a [String] array and transforms it to a [[String]] array, where each internal array corresponds to one line that will fit horizontally in my view. (Technically it starts with a String that is split on whitespace to form the [String], but in a generalized sense, I've got a collection I want to split into multiple lines.) Then I can lay it out using VStack, HStack, and ForEach.
My first approach was to try to read the widths off the actual views I'm displaying. However, I quickly ran into infinite recursions or weirdly unstable oscillations because it might truncate a Text view (e.g. [Four] [score] [and] [se...]), and then un-truncate once once the reflow changed, back and forth (or just end in a truncated state.
So I decided to cheat. I lay out all the words in a second, invisible horizontal scrollview. This way, they all get to take up as much space as they want, never get truncated, and most importantly, because this layout only depends on the [String] array and not the derived [[String]] array, it can never enter a recursive loop. You may think that laying each view twice (once for measuring width and once for displaying) is inefficient, but I found it to be dozens of times faster than trying to measure the widths from the displayed views, and to produce proper results 100% of the time.
+---------- FIRST TRY - CYCLIC ----------+ +-------- SECOND TRY - ACYCLIC --------+
| | | |
| +--------+ [String] +----------+ | | +-------+ [String] +--------+ |
| | | | | | | |
| | +--------------------------+ | | | v v |
| | | | | | | Hidden +--> Widths +--> [[String]] |
| v v + v | | layout | |
| Display +--> Widths +--> [[String]] | | v |
| layout | | Display |
| | | layout |
+----------------------------------------+ +--------------------------------------+
To read and save the widths, I adapted the GeometryReader/PreferenceKey approach detailed on swiftui-lab.com. The widths are saved in the view model, and updated whenever the number or size of views in the hidden scrollview change. Such a change (or changing the width of the view) then reflows the [String] array to [[String]] based on the widths saved in the model.
Summary
Now, whether any of this is useful in a shipping application will depend on how many items you want to reflow, and whether they will be static once laid out or changing often. But I found it to be a fascinating diversion!
It's a two step process using a GeometryReader.
Measure for each item the width of content(item).
Use measurements to lay items out into rows.
Only problem is that it has to recalculate on each redraw or cache the priorly measured width, which is not necessarily a problem with just a few items though.
I won't post the code here since it uses GeometryReader which is not something the author wants to use.
Related
Was wondering if there is a way to set a dynamic space between words inside a UILabel to obtain something like this
| |
|WordA -dynamic_space_to_fill_the_label- WordB|
| |
Where the | represent the borders of my UILabel.
It would be like I had left alignment for WordA and right alignment for WordB.
Any suggestion on how to do this?
Imagine you have four or so views, all width 100, different heights. You have a wrapper view W which holds them all.
A |
B | W
C |
D |
the heights of the small views can change. At that time you want them all to move, float, appropriately, and resize W.
Now, I was just about to write a few lines of code to do this.
So .. (1) you'd have W find all the subviews and list them in order from top to bottom. Then (2) each time there is a change, you'd (3) reposition each of ABCD. the position of each one is the sum of the heights of the items above it, and (4) resize W to the sum of all heights.
Now that's all fine but -- idiots reinvent the wheel!
Am I missing something obvious in iOS? is there already a package everyone uses to do this all the time? Or something built in? What's the situation?
(Note that of course frustratingly, for our Android friends this is built in! And of course any web-html system does this automatically.)
What's the right engineering solution for iOS views here? For the record this is iOS7+ only, no old-fashioned stuffs need be covered, if it makes a difference. Cheers
(1) you'd have W find all the subviews and list them in order from top
to bottom. Then (2) each time there is a change, you'd (3) reposition
each of ABCD. the position of each one is the sum of the heights of
the items above it, and (4) resize W to the sum of all heights.
You can use constraints in Interface Builder for that whole process, no code required. Do this:
set the width of subview A to 100
constrain B, C, and D to match A's width
add vertical spacing constraints between A and B, B and C, and C and D to maintain their relative position
add a vertical spacing constraint between W (the superview, shown in gray) and A
add a vertical spacing constraint between W and D
add leading and trailing space constraints between W and view A
You'll end up with something that looks like this:
The constraints editor in Xcode isn't completely intuitive, but it is easy to use once you understand what you can and can't do with constraints in IB and when you need to use code to set up the constraints.
I have a UIControl subclass that changes appearance and size when touched, a bit like the button of an iOS keyboard turns into the typewriter hand when touched:
I’m not sure how to write it. The idea is to draw a different shape when the default button shape is highlighted:
+---+
| |
+---+ | |
| | -> touch -> | |
+---+ +---+
But since the default button is smaller than the highlighted shape, the bigger shape gets clipped. I have considered these options:
Drawing outside the default frame. Does not seem to be possible.
Changing the size of the control when highlighted. Feels quite hacky and error-prone.
Enlarging the control to accomodate the larger shape. Feels “wrong”, I’d like to be able to work with the control in its smaller, default shape.
Is there a best practice for this scenario?
Here are a few approaches:
You can draw outside the frame, you just have to make sure that myView.clipsToBounds is False. This would allow you to draw outside the bounds of the view inside another view and draw whatever extra content you would like.
That is probably the cleanest solution, but is also a bit "hacky" in my opinion. The other approach would be dirty (as you said) and to reset the frame size.
The last approach would be to create another view (the key pressed down) and throw it on top of the original (unpressed key) view.
All in all, it really comes down to what you're drawing / showing inside your view.
Hi am developing jigsaw puzzle for iPhone.
Here i have almost completed the app except finding whether the jigsaw is complete.
Here each piece is a UIImageView, and it has unique tags.
I have tried using x y coordinates, but its no use because user can start fix the puzzle from anywhere in the screen.
So, any idea how to detect whether the images are arranged correctly?
Edit: Thanks a lot all finally i have done it using co ordinates. by tracing location and tags.
Presumably, you have some sort of 'snap to grid' mechanism to put each piece precisely into the correct position once it's close (as would effectively occur with a real jigsaw). Let's say that this is defined as a specific x,y co-ordinate for the origin of the UIImageView for each piece.
Then, when each piece is in it's correct position (snapped to it's position), set a boolean 'inPlace' for that piece to YES.
When the value for 'inPlace' for all pieces is YES, the jigsaw is complete.
Check out my question here: Check how close UILabels are to each other
It is the same, except I am asking about labels, but you can try to use that with images. I am pretty sure there is a different translation scheme however
I have tried using x y coordinates, but its no use because the user can start fixing it from anywhere in the screen.
You have to select one "base" jigsaw, and get relative coordinates against it for others. Completion check - every piece keeps stored relative position equal to current. Seems, thats all.
Another case if you want to fix puzzle starting random piece and gather by parts (like torn apart image or letter - pieces have different shapes). In this case, for each jigsaw you have to store closest neighbors ids and theirs relative position. If two neighbor pieces are in place - join them. Condition to win - all neighbors for all pieces are in relative place.
What about labeling the pieces with id's
Each piece has something like
JigsawPiece.Id = 1
and label them like this (for a 3x3 puzzle);
-------------
| 0 | 1 | 2 |
-------------
| 3 | 4 | 5 |
-------------
| 6 | 7 | |
-------------
Add these pieces in a 2D array
class JigsawPuzzle:
array[3][3] JigsawPiece;
And finally check if the solution is reached by looking if the numbers are increasing by one.
check_id == 1
for int i = 0; i<numrows ; i++
for int j = 0; j<numcols; j++{
if i==numrows - 1 and j==numcols-1: break #last one, which should be empty, skip it
solved = jigsawpieces[col][row].id == check_id
check_id += 1
if !solved: break;
}
if solved: You solved it!
All is pseudo code because I don't know ObjC
Something along these lines should do the trick!
How can I set the column width in comments for doxygen?
For the following table I would like to have the first column as small as possible in my LaTeX (PDF) output.
/*!
#brief
blablabla
Name | Description
---- | -----------
AB | asdf asdf asdf asdf asdf
*/
Latex has two ways to make tables (at least as far as I know):
with minimal width, but no text wrapping or protection that prevents the table from becoming too wide. You simple get an overfull warning and the table will run off of the page.
with a fixed column width; then the text will nicely wrap but one has to select an appropiate width for each column in advance.
Since doxygen cannot guess the table's width, it uses a fixed column width, and currently that is based on \textwidth divided by the number of columns.
I'm thinking about a putting that width in a TeX length variable, so you can overrule it with a special doxygen command, but this has not been implemented yet.
I'm using Doxygen 1.8.9.1, and AFAIK today still no Doxygen option exists to turn off the fixed-width in columns. However, you can edit the doxygen.sty file manually.
I was able to achieve what you want by searching for \begin{xtabular} and changing the column markup (i.e. what is between two vertical bars |) for all columns except the last. The p{x.xx\textwidth} command (don't know if I'm using the correct LaTeX terminology) defines the width of each column. By trying, it seems better to also replace the command before that (>{\centering} and >{\raggedleft\hspace{0pt}}).
For example,
\begin{xtabular}{|>{\centering}p{0.10\textwidth}|%
>{\raggedleft\hspace{0pt}}p{0.15\textwidth}|%
p{0.678\textwidth}|}}%
would then become:
\begin{xtabular}{|c|%
r|%
p{0.678\textwidth}|}}%
Ditching the empty comments and putting everything on one line gives:
\begin{xtabular}{|c|r|p{0.678\textwidth}|}}
The two-step process then becomes a three-step process: to create a pdf, you
run Doxygen,
adapt the generated doxygen.sty in the latex subfolder or replace it with your edited version,
run Make.bat.
Notes:
This of course als means that all your tables end up having a different width.
You talk about shrinking the widths of columns, but depending on your naming conventions you possibly end up enlarging your column width (thereby resolving a very ugly looking overflow layout problem). Make sure the last column doesn't fall of the right edge of your paper by making the width of the last column also fluid, or by setting it to a fixed value which is low enough for even the longest names.