UI Part 2 (UICollectionView Part 2)
This is the continuation of the previous part. It is mandatory that you went through it. We will use the same project that we created previously. Build and run the starter project, you will see the vertical collection View and the example cover here is inspired by Raywenderlich
Objective 1
Our first objective is, the cell at the center become more prominent than others. Since the layout in this case also the grid based vertical layout we still need UICollectionViewFlowLayout
but we need to add some extra decorative logic to make the center cell more prominent for that we need to subclass UICollectionViewFlowLayout
as we discussed in the previous blog. We subclassed UICollectionViewFlowLayout
when we need grid line based behaviour but with custom behaviour
As shown in Figure 2 , we did a few things
- Created a
ProminentCellLayout
subclass ofUICollectionViewFlowLayout
- This class responsibilities is to layout object and as you can see we removed
UICollectionViewDelegateFlowLayout
method as well, though you can still use these method to tell the size and other properties , but to put this logic in our custom class for two reason 1. Reusability (if you want this type of layout in other view you can assign only this layout and it will works as expected) 2. Performance (since our item size is hardcoded , we assign only one time instead of every time cell is render)
prepare()
- Override this method to calculate the position and size of every cell, as well as the total dimensions for entire layout
- Subclasses should always call super if they override.
- After this method return ,
UICollectionView
decided it’s scrolling content size , if you don’t implementsizeForItemAt
, otherwisesizeForItemAt
have overwrite theitemSize
property - You will see
prepare()
method usage when we actually subclassUICollectionViewLayout
- This method only calls once or whenever the layout is invalidated and as you can see in Figure 3, this method only calls one time and in our device we scrolled to the bottom. The
isSetup
flag we added to protect invalidated case - We will see this in details when we will be working with
UICollectionViewLayout
directly, though in our case we used to put layout logic in one class
UICollectionViewLayoutAttributes
- A layout object that manages the layout-related attributes for a given item in a collection view.
- Layout objects create instances of this class when asked to do so by the collection view. In turn, the collection view uses the layout information to position cells and supplementary views inside its bounds.
let’s understand this in deeper with figure 4 and 5, Build and run the application , few things to note
layoutAttributesForElements
is called once from each visible frame and asks us to change attributes or return the attributes that is only created by UICollectionViewFlowLayout. You don’t need to overwrite this method if you are not changing any attributes- As shown we can see it asks us to give cells attributes from index 0 to 3 since only four cells are visible
- Let’s consider information it holds ,
attributes![0]
holds all the information to place cell at index0 . frame = (87.5, 0.0, 200.0, 200.0), size(200,200), alpha = 1
you can validate with the Figure as well. - You can subclass
UICollectionViewLayoutAttributes
and define whatever properties you want to store the additional layout data.
Update UICollectionViewLayoutAttributes
As shown in Figure 6 we safely update the attributes, Safely means we didn’t remove layoutAttributesForElements
that is created by Layout object . Instead we first copy all attributes and update then return a new copy much like functional programming .
You are thinking 🤔 we can do this when displaying the cell but when displaying the cell we don’t have any information related to which cell currently is in center . The information we only get in this method
Some people thinking, 🤔 we can do this by implementing UIScrollView
delegate , and the answer is yes we can do there but if we give the responsibility to one class they should do this.
Some people thinking, 🤔 , we are changing alpha
value here , and the responsibility of that class to hold layout information not to do any UI stuff so we are breaking SRP, yes But there are other people as well who says If I change alpa value here and If I need this logic in other collection View only I just give them this layout not to duplicate code of alpha value in other collection view . In next section I will cover how to do UI stuff in UICollectionViewCell
,
Alternative Approach
There is also an alternative approach as well , where we will do this when displaying the cell , for that we need to add some new properties on layout object that keeps track which cell is currently in center
As shown in Figure 7 we did few things
- Created subclass of
UICollectionViewLayoutAttributes
- Added additional property
isCenter
to track which cell is is center - Declared your custom attribute subclass class name by
override class var layoutAttributesClass: AnyClass
in yourUICollectionViewLayout
class. The system calls this class method to see if there is a custom class to be supplied when you use the factory method for instantiating/dequeuing layout attribute objects. If you don’t provide this class application will crash - Implement custom property, be sure to implement an override for -
(id)copyWithZone:
or theUICollectionView
will lose any custom values you have applied to your custom collection view object. Further you need to overridefunc isEqual(_ object: Any?) -> Bool
as well - On
layoutAttributesForElements
method we assign value on the basis of layout information we got at that point . We access this property by cast it as MyAttributeClass.
Note : ignore force unwrap things , you are noticing everywhere in this blog series
As shown in Figure 8 we finally changing alpha value on our custom cell. Few things
- Classes that want to support custom layout attributes specific to a given
UICollectionViewLayout
subclass can apply them here. - -
applyLayoutAttributes:
is then called after the view is added to the collection view and just before the view is returned from the reuse queue. - Note that
-applyLayoutAttributes
: is only called when attributes change, as defined by -isEqual:.
layoutAttributesForElements
- This function returns an array of layout attributes for all the cells and views within the collection view visible rectangle .
- You step through these attributes of the cell and can modify depending on your layout information you get
- You are familiar this methods as we used this method perviously
- You only need to override this method when your intend is to change some attributes
Focus On Objective 1
Now removed UICollectionViewLayoutAttributes
custom class . we need to do some math that we reduce alpha and scale on the basis of cell distance from the center of CollectionView and the alpha is reduces minimum to 0.5. If the cell is is center it’s alpha will be 1.0
Step 1: Find the Distance between Center of Cell and The CollectionView Center
As shown in Figure 9 we find the distance
If you want to see How this method will find the distance between center let’s look Figure 10 and consider Cell3, Here are the points
collectionCenter
we get the collectionView center , By dividing its visible frame height / 2, Let’ see collection View height = 400 , so the center is always be the200
- Since each cell height is
200
, previous 400 space is filled by the cell 1 and 2 and since user scrolls to the cell 3 it’s content size position is from 400 to 600, plz ignore line spacing and status bar inset so its center.y value will be atcenter_of_cell_3 = 500
- Since user scrolls Cell 1 and cell 2 completely and they are not in the frame so the content offset will be
content_offset = 400,
(Cell1 height + Cell 2 height ) - So normalized center of cell 3 with respect to current visible area will be 100 (
normalized center
=center_of_cell_3 — content_offset
) - If cell 3 center is at 200 you can say that cell 3 is completely center on the frame , but the distance of cell 3 center with the frame center is 100 (
normalized center — collectionCenter
) . Note we take absolute value becausenormalized center
can be greater thancollectionCenter
.
Step 2: If the distance is greater or equal to the cell height , then take cell height
let distance = min(abs(collectionCenter — normalizedCenter), maxDistance)
Step 3: Find ratio
Distance = 0 , ratio = 1
Distance = 200, ratio = 0
let ratio = (maxDistance — distance) / maxDistance
Step 4: Find alpha and scale Value
ratio = 0 , alpha = 0.5, scale 0.5
ratio =0.5 , alpha = 0.75, scale 0.75
ratio =1 , alpha = 1, scale 1
let standardItemAlpha: CGFloat = 0.5let standardItemScale: CGFloat = 0.5let alpha = ratio * (1 — standardItemAlpha) + standardItemAlphalet scale = ratio * (1 — standardItemScale) + standardItemScale
Objective 1 : Cell Closest to Center will have alpha and scale value 1
As shown in Figure 11 since cell 2 is closest to center it has hight alpha and scale greater than 0.5
But we have one problem when you scroll layout will not updated, because as we scroll , the collection view will need to update it’s layout . To get the layout updated we need to override shouldInvalidateLayout
method. return YES to cause the collection view to requery the layout for geometry information. One thing to note after we implemented shouldInvalidateLayout ,
when you scroll it invalidated the layout and prepare will call , now at this point out flag will work
Objective 2 we want at a time only one cell will be prominent , like in this case since cell 3 is closest to the Frame center it should be in center. So we need to snap the cell to center which is closest
Meet targetContentOffset
targetContentOffsetForProposedContentOffset
return a point at which to stop scrolling- It has
proposedContentOffset
parameter which is point at which scrolling naturally stops - By overriding this method we can set scrolling to snap to specific boundary
- We did a calculation after user naturally stops, and from that location who is the closest cell , and what additional offset we need further to move it to the center
Calculation
- Let’s say
collection view center is 333.5
- User scroll and
proposedContentOffset is 11.5
proposed center = 345
offset
=proposed center — collection view center
Cells Self-Sizing
As shown in Figure 15 we are creating tag cell , we have collection view in blue having 400 height
, and cell in red and you can see we don’t specify cell width and height at all and collection view used default width
and height
which is 50
as shown in Figure 16,
Problem : we want cell to use it’s intrinsic content size or in short expand on the basis of it’s content (Dynamic Size)
Meet estimatedItemSize
By default UICollectionViewFlowLayout uses itemSize property for defining the sizes of the cells. Another way is to use UICollectionViewDelegateFlowLayout and supplying item sizes by implementing collectionView(_:layout:sizeForItemAt:). Third way is to let auto layout for defining the size of the cell
Dynamic cells sizing is an opt-in feature of UICollectionViewFlowLayout
, which could be enabled by setting estimatedItemSize
property to a non-zero value. Once estimated size has been set, the flow layout computes a first approximation of cells arrangement. The layout is re-calculated when the updated attributes are received. Hence it boosts performance if estimated size is close to the actual one.
Set this constant as the value for the estimatedItemSize property to enable self-sizing cells for your collection view. This is a non-zero, placeholder value that tells the collection view to query each cell for its actual size using the cell’s preferredLayoutAttributesFitting(_:) method.
Providing an estimated cell size can improve the performance of the collection view when the cells adjust their size dynamically. The estimated value lets the collection view defer some calculations to determine the actual size of its content. Cells that aren’t onscreen are assumed to be the estimated height.The default value of this property is CGSizeZero
. Setting it to any other value, like automaticSize
, causes the collection view to query each cell for its actual size using the cell’s preferredLayoutAttributesFitting(_:)
method.If all of your cells are the same size, use the itemSize
property, instead of this property, to specify the cell size instead.
As shown in Figure 18, Four Item text exceeding from device boundary
Solution
Use preferredMaxLayoutWidth
(The preferred maximum width, in points, for a multiline label.)
This property affects the size of the label when the system applies layout constraints to it. During layout, if the text extends beyond the width specified by this property, the additional text flows to one or more new lines, increasing the height of the label.
Now we run into another we want collection view to take heigh as per content , now you can see we don’t want scrolling , In short we want dynamic height of collection view on the basis of it’s content
As clear from view debugger collection view height = 100 and collection view content size
= 154
Content Size:
Content Size represent the width and height of all the content, not just the content that is currently visible. The collection view uses this information to configure its own content size for scrolling purposes. if content size > size = perform scrolling
154 > 100 == it will perform scrolling
Intrinsic content size
Most views have an intrinsic content size, which refers to the amount of space the view needs for its content to appear in an ideal state. For example, the intrinsic content size of a UILabel
will be the size of the text it contains using whatever font you have configured it to use.
Intrinsic content sizes are important because they allow views to have a natural width and height without us forcing one. For Auto Layout to work it must know where each view is positioned precisely: its X, Y, width, and height values. With intrinsic content size we can say “place this button 20 points from the top and center it horizontally” and that’s enough to form a complete layout — Auto Layout can calculate the rest based on the button’s intrinsic size.
we need the UITableViewCell
to adjust its height dynamically according to its content. Also, the UICollectionView
must be such that all the cells are displayed in one go, i.e. no scrolling allowed. Our strategy would be we will enable autolayout in cell to calculate cell height and cell will use the intrinsic size of collection view , for collection view intrinsic size we will inject the actual content size in collection view intrinsic size
Step 1 Add Table View
Blue color
Step 2 UITableViewCell With Collection View
Yellow Color
Step 3 Collection View Data Source
Collection view cell is Red color
Run the application and you can see cell height not taking collections view content size, In collection view you need to scroll to see the content
Solution
Dynamically calculating the collectionView’s
height as per its contentSize
is simply a 3 step process.
Dynamically calculating the collectionView’s
height as per its contentSize
is simply a 3 step process.
- Subclass
UICollectionView
and override itslayoutSubviews()
andintrinsicContentSize
, The above code will invalidate theintrinsicContentSize
and will use the actualcontentSize
ofcollectionView
. The above code takes into consideration thecustom layout
as well.
- . Now, set
DynamicHeightCollectionView
as thecollectionView’s
class and isScrollEnabled = false
- One last thing, for the changes to take effect: you need to call
layoutIfNeeded()
oncollectionView
, after reloadingcollectionView’s
data, i.e.
As shown in Figure 24 it is working fine but tableview view cell is taking more extra space
Next Part
In the next part we will subclass UICollectionViewLayout
and build a MosaicLayout