UI Part 2 (UICollectionView Part 2)

https://developer.apple.com/documentation/uikit/uicollectionview

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

Figure 1

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

  1. Created a ProminentCellLayout subclass of UICollectionViewFlowLayout
  2. 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)
Figure 2.1
Figure 2.2

prepare()

  1. Override this method to calculate the position and size of every cell, as well as the total dimensions for entire layout
  2. Subclasses should always call super if they override.
  3. After this method return , UICollectionView decided it’s scrolling content size , if you don’t implement sizeForItemAt, otherwise sizeForItemAt have overwrite the itemSize property
  4. You will see prepare() method usage when we actually subclass UICollectionViewLayout
  5. 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
  6. 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
Figure 3

UICollectionViewLayoutAttributes

  1. A layout object that manages the layout-related attributes for a given item in a collection view.
  2. 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

  1. 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
  2. As shown we can see it asks us to give cells attributes from index 0 to 3 since only four cells are visible
  3. Let’s consider information it holds , attributes![0] holds all the information to place cell at index 0 . frame = (87.5, 0.0, 200.0, 200.0), size(200,200), alpha = 1you can validate with the Figure as well.
  4. You can subclass UICollectionViewLayoutAttributes and define whatever properties you want to store the additional layout data.
Figure 4
Figure 5

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 ,

Figure 6

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

  1. Created subclass of UICollectionViewLayoutAttributes
  2. Added additional property isCenter to track which cell is is center
  3. Declared your custom attribute subclass class name by override class var layoutAttributesClass: AnyClass in your UICollectionViewLayout 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
  4. Implement custom property, be sure to implement an override for -(id)copyWithZone: or the UICollectionView will lose any custom values you have applied to your custom collection view object. Further you need to override func isEqual(_ object: Any?) -> Bool as well
  5. 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

Figure 7

As shown in Figure 8 we finally changing alpha value on our custom cell. Few things

  1. Classes that want to support custom layout attributes specific to a given UICollectionViewLayout subclass can apply them here.
  2. -applyLayoutAttributes: is then called after the view is added to the collection view and just before the view is returned from the reuse queue.
  3. Note that -applyLayoutAttributes: is only called when attributes change, as defined by -isEqual:.
Figure 8

layoutAttributesForElements

  1. This function returns an array of layout attributes for all the cells and views within the collection view visible rectangle .
  2. You step through these attributes of the cell and can modify depending on your layout information you get
  3. You are familiar this methods as we used this method perviously
  4. 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

Figure 9

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

  1. 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 the 200
  2. 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 at center_of_cell_3 = 500
  3. 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 )
  4. So normalized center of cell 3 with respect to current visible area will be 100 ( normalized center = center_of_cell_3 — content_offset)
  5. 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 because normalized center can be greater than collectionCenter.
Figure 10

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.5
let 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

Figure 11

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

  1. targetContentOffsetForProposedContentOffset return a point at which to stop scrolling
  2. It has proposedContentOffset parameter which is point at which scrolling naturally stops
  3. By overriding this method we can set scrolling to snap to specific boundary
  4. 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

  1. Let’s say collection view center is 333.5
  2. User scroll and proposedContentOffset is 11.5
  3. proposed center = 345
  4. offset = proposed center — collection view center
Figure 12

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)

Figure 13
Figure 14
Figure 15
Figure 16

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.

Figure 17

As shown in Figure 18, Four Item text exceeding from device boundary

Figure 18

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.

Figure 19

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

https://www.freecodecamp.org/news/how-to-make-height-collection-views-dynamic-in-your-ios-apps-7d6ca94d2212/

Step 1 Add Table View

Blue color

Figure 20

Step 2 UITableViewCell With Collection View

Yellow Color

Figure 21

Step 3 Collection View Data Source

Collection view cell is Red color

Figure 22

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

Figure 23

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.

  1. Subclass UICollectionView and override its layoutSubviews() and intrinsicContentSize , The above code will invalidate the intrinsicContentSize and will use the actual contentSize of collectionView. The above code takes into consideration the custom layout as well.
  1. . Now, set DynamicHeightCollectionView as the collectionView’s class and isScrollEnabled = false
  1. One last thing, for the changes to take effect: you need to call layoutIfNeeded() on collectionView, after reloading collectionView’s data, i.e.

As shown in Figure 24 it is working fine but tableview view cell is taking more extra space

Figure 24

Next Part

In the next part we will subclass UICollectionViewLayout and build a MosaicLayout

Useful links

Senior iOS Engineer | HungerStation | Delivery Hero