Layout Performance Optimization
Layout performance is crucial for maintaining smooth user interactions in mobile applications. This section covers layout optimization techniques, constraint optimization, and responsive design patterns that minimize layout calculations and improve rendering performance.
Constraint Layout Optimization
Android Layout Performance
kotlin
// Android - Optimized ConstraintLayout usage
class PerformantLayoutActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Use view binding for better performance
val binding = ActivityPerformantLayoutBinding.inflate(layoutInflater)
setContentView(binding.root)
setupOptimizedLayout(binding)
}
private fun setupOptimizedLayout(binding: ActivityPerformantLayoutBinding) {
// Use chains for better performance than nested layouts
val constraintSet = ConstraintSet()
constraintSet.clone(binding.constraintLayout)
// Create horizontal chain
constraintSet.createHorizontalChain(
ConstraintSet.PARENT_ID,
ConstraintSet.LEFT,
ConstraintSet.PARENT_ID,
ConstraintSet.RIGHT,
intArrayOf(R.id.leftView, R.id.centerView, R.id.rightView),
null,
ConstraintSet.CHAIN_SPREAD_INSIDE
)
constraintSet.applyTo(binding.constraintLayout)
}
}
// Custom ViewGroup for complex layouts
class OptimizedLinearLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ViewGroup(context, attrs, defStyleAttr) {
private var totalLength = 0
private val childMeasureSpecs = mutableListOf<MeasureSpec>()
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
// Cache child measure specs to avoid repeated calculations
if (childMeasureSpecs.size != childCount) {
childMeasureSpecs.clear()
repeat(childCount) {
childMeasureSpecs.add(MeasureSpec())
}
}
var maxWidth = 0
totalLength = 0
// Measure children efficiently
for (i in 0 until childCount) {
val child = getChildAt(i)
if (child.visibility == GONE) continue
val lp = child.layoutParams as MarginLayoutParams
// Use cached measure spec if dimensions haven't changed
val childWidthMeasureSpec = getChildMeasureSpec(
widthMeasureSpec,
paddingLeft + paddingRight + lp.leftMargin + lp.rightMargin,
lp.width
)
val childHeightMeasureSpec = getChildMeasureSpec(
heightMeasureSpec,
paddingTop + paddingBottom + totalLength + lp.topMargin + lp.bottomMargin,
lp.height
)
child.measure(childWidthMeasureSpec, childHeightMeasureSpec)
totalLength += child.measuredHeight + lp.topMargin + lp.bottomMargin
maxWidth = maxOf(maxWidth, child.measuredWidth + lp.leftMargin + lp.rightMargin)
}
// Add padding
totalLength += paddingTop + paddingBottom
maxWidth += paddingLeft + paddingRight
setMeasuredDimension(
resolveSizeAndState(maxWidth, widthMeasureSpec, 0),
resolveSizeAndState(totalLength, heightMeasureSpec, 0)
)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
var childTop = paddingTop
for (i in 0 until childCount) {
val child = getChildAt(i)
if (child.visibility == GONE) continue
val lp = child.layoutParams as MarginLayoutParams
childTop += lp.topMargin
val childLeft = paddingLeft + lp.leftMargin
child.layout(
childLeft,
childTop,
childLeft + child.measuredWidth,
childTop + child.measuredHeight
)
childTop += child.measuredHeight + lp.bottomMargin
}
}
override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams {
return MarginLayoutParams(context, attrs)
}
override fun generateDefaultLayoutParams(): LayoutParams {
return MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
}
data class MeasureSpec(
var widthSpec: Int = 0,
var heightSpec: Int = 0
)
}
// Layout inflation optimization
class LayoutOptimizer {
private val layoutInflaterCache = LruCache<Int, View>(20)
fun inflateOptimized(layoutId: Int, parent: ViewGroup, attachToRoot: Boolean = false): View {
// Check cache first for reusable views
val cachedView = layoutInflaterCache.get(layoutId)
if (cachedView != null && cachedView.parent == null) {
if (attachToRoot) {
parent.addView(cachedView)
}
return cachedView
}
// Inflate with optimized inflater
val inflater = LayoutInflater.from(parent.context)
val view = inflater.inflate(layoutId, parent, attachToRoot)
// Cache for reuse if it's a commonly used layout
if (isReusableLayout(layoutId)) {
layoutInflaterCache.put(layoutId, view)
}
return view
}
private fun isReusableLayout(layoutId: Int): Boolean {
// Define which layouts are suitable for reuse
return when (layoutId) {
R.layout.list_item_simple,
R.layout.card_view_basic -> true
else -> false
}
}
}
iOS Auto Layout Optimization
swift
// iOS - Auto Layout performance optimization
class OptimizedViewController: UIViewController {
private var cachedConstraints: [NSLayoutConstraint] = []
private var constraintAnimator: UIViewPropertyAnimator?
override func viewDidLoad() {
super.viewDidLoad()
setupOptimizedLayout()
optimizeConstraintActivation()
}
private func setupOptimizedLayout() {
// Create views
let headerView = createHeaderView()
let contentView = createContentView()
let footerView = createFooterView()
// Add to view hierarchy
view.addSubview(headerView)
view.addSubview(contentView)
view.addSubview(footerView)
// Batch constraint activation for better performance
let constraints = [
// Header constraints
headerView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
headerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
headerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
headerView.heightAnchor.constraint(equalToConstant: 60),
// Content constraints
contentView.topAnchor.constraint(equalTo: headerView.bottomAnchor),
contentView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: footerView.topAnchor),
// Footer constraints
footerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
footerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
footerView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
footerView.heightAnchor.constraint(equalToConstant: 80)
]
// Activate all constraints at once
NSLayoutConstraint.activate(constraints)
cachedConstraints = constraints
}
private func optimizeConstraintActivation() {
// Use priority-based constraints to avoid conflicts
cachedConstraints.forEach { constraint in
if constraint.firstAttribute == .height || constraint.firstAttribute == .width {
constraint.priority = UILayoutPriority(999) // Less than required
}
}
}
// Efficient constraint updates
func updateLayoutWithAnimation() {
// Disable layout during constraint changes
view.translatesAutoresizingMaskIntoConstraints = false
// Batch constraint changes
UIView.performWithoutAnimation {
// Update constraint constants
cachedConstraints.first { $0.firstAttribute == .height }?.constant = 100
// Mark for layout update
view.setNeedsLayout()
}
// Animate layout changes
constraintAnimator = UIViewPropertyAnimator(duration: 0.3, curve: .easeInOut) {
self.view.layoutIfNeeded()
}
constraintAnimator?.startAnimation()
}
private func createHeaderView() -> UIView {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .systemBlue
return view
}
private func createContentView() -> UIView {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .systemBackground
return view
}
private func createFooterView() -> UIView {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .systemGray
return view
}
}
// Custom layout for complex scenarios
class GridLayout: UIView {
private var itemSize: CGSize = CGSize(width: 100, height: 100)
private var spacing: CGFloat = 8
private var cachedFrames: [CGRect] = []
override func layoutSubviews() {
super.layoutSubviews()
// Only recalculate if necessary
if cachedFrames.count != subviews.count || bounds.size != cachedBounds {
calculateItemFrames()
cachedBounds = bounds.size
}
// Apply cached frames
for (index, subview) in subviews.enumerated() {
if index < cachedFrames.count {
subview.frame = cachedFrames[index]
}
}
}
private var cachedBounds: CGSize = .zero
private func calculateItemFrames() {
cachedFrames.removeAll()
let itemsPerRow = Int((bounds.width + spacing) / (itemSize.width + spacing))
for (index, _) in subviews.enumerated() {
let row = index / itemsPerRow
let col = index % itemsPerRow
let x = CGFloat(col) * (itemSize.width + spacing)
let y = CGFloat(row) * (itemSize.height + spacing)
cachedFrames.append(CGRect(origin: CGPoint(x: x, y: y), size: itemSize))
}
}
override var intrinsicContentSize: CGSize {
let itemsPerRow = Int((bounds.width + spacing) / (itemSize.width + spacing))
let numberOfRows = (subviews.count + itemsPerRow - 1) / itemsPerRow
let height = CGFloat(numberOfRows) * itemSize.height + CGFloat(numberOfRows - 1) * spacing
return CGSize(width: bounds.width, height: height)
}
}
// Stack view optimization
extension UIStackView {
func optimizePerformance() {
// Reduce relayout frequency
distribution = .fillEqually
// Use consistent spacing
spacing = 8
// Disable automatic distribution for better performance
if #available(iOS 11.0, *) {
setCustomSpacing(8, after: arrangedSubviews.first!)
}
}
func addArrangedSubviewsEfficiently(_ views: [UIView]) {
// Batch additions to avoid multiple layout passes
views.forEach { view in
view.translatesAutoresizingMaskIntoConstraints = false
}
// Add all views at once
UIView.performWithoutAnimation {
views.forEach { addArrangedSubview($0) }
}
}
}
Responsive Design Patterns
Flutter Layout Optimization
dart
// Flutter - Responsive layout with performance optimization
class ResponsiveLayout extends StatelessWidget {
final Widget mobile;
final Widget tablet;
final Widget desktop;
const ResponsiveLayout({
Key? key,
required this.mobile,
required this.tablet,
required this.desktop,
}) : super(key: key);
static bool isMobile(BuildContext context) =>
MediaQuery.of(context).size.width < 768;
static bool isTablet(BuildContext context) =>
MediaQuery.of(context).size.width >= 768 &&
MediaQuery.of(context).size.width < 1200;
static bool isDesktop(BuildContext context) =>
MediaQuery.of(context).size.width >= 1200;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth >= 1200) {
return desktop;
} else if (constraints.maxWidth >= 768) {
return tablet;
} else {
return mobile;
}
},
);
}
}
// Optimized flexible layout
class FlexibleOptimizedLayout extends StatelessWidget {
final List<Widget> children;
final MainAxisAlignment mainAxisAlignment;
final CrossAxisAlignment crossAxisAlignment;
final MainAxisSize mainAxisSize;
const FlexibleOptimizedLayout({
Key? key,
required this.children,
this.mainAxisAlignment = MainAxisAlignment.start,
this.crossAxisAlignment = CrossAxisAlignment.center,
this.mainAxisSize = MainAxisSize.max,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
// Choose layout direction based on available space
final isWideScreen = constraints.maxWidth > constraints.maxHeight;
return Flex(
direction: isWideScreen ? Axis.horizontal : Axis.vertical,
mainAxisAlignment: mainAxisAlignment,
crossAxisAlignment: crossAxisAlignment,
mainAxisSize: mainAxisSize,
children: children.map((child) {
// Wrap in Flexible for responsive behavior
return Flexible(
flex: 1,
child: RepaintBoundary(child: child),
);
}).toList(),
);
},
);
}
}
// Custom layout delegate for complex layouts
class CustomGridDelegate extends SliverGridDelegate {
final double maxCrossAxisExtent;
final double childAspectRatio;
final double crossAxisSpacing;
final double mainAxisSpacing;
const CustomGridDelegate({
required this.maxCrossAxisExtent,
this.childAspectRatio = 1.0,
this.crossAxisSpacing = 0,
this.mainAxisSpacing = 0,
});
@override
SliverGridLayout getLayout(SliverConstraints constraints) {
final usableCrossAxisExtent = constraints.crossAxisExtent - crossAxisSpacing;
final childCrossAxisExtent = math.min(maxCrossAxisExtent, usableCrossAxisExtent);
final childMainAxisExtent = childCrossAxisExtent / childAspectRatio;
final crossAxisCount = math.max(1, usableCrossAxisExtent ~/ (childCrossAxisExtent + crossAxisSpacing));
return SliverGridRegularTileLayout(
crossAxisCount: crossAxisCount,
mainAxisStride: childMainAxisExtent + mainAxisSpacing,
crossAxisStride: childCrossAxisExtent + crossAxisSpacing,
childMainAxisExtent: childMainAxisExtent,
childCrossAxisExtent: childCrossAxisExtent,
reverseCrossAxis: axisDirectionIsReversed(constraints.crossAxisDirection),
);
}
@override
bool shouldRelayout(CustomGridDelegate oldDelegate) {
return oldDelegate.maxCrossAxisExtent != maxCrossAxisExtent ||
oldDelegate.childAspectRatio != childAspectRatio ||
oldDelegate.crossAxisSpacing != crossAxisSpacing ||
oldDelegate.mainAxisSpacing != mainAxisSpacing;
}
}
// Optimized wrap layout
class OptimizedWrap extends StatelessWidget {
final List<Widget> children;
final WrapAlignment alignment;
final double spacing;
final double runSpacing;
const OptimizedWrap({
Key? key,
required this.children,
this.alignment = WrapAlignment.start,
this.spacing = 0.0,
this.runSpacing = 0.0,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
// Pre-calculate layout to avoid expensive operations during build
final wrappedChildren = _optimizeChildLayout(constraints);
return Wrap(
alignment: alignment,
spacing: spacing,
runSpacing: runSpacing,
children: wrappedChildren,
);
},
);
}
List<Widget> _optimizeChildLayout(BoxConstraints constraints) {
return children.map((child) {
return RepaintBoundary(
child: IntrinsicWidth(
child: child,
),
);
}).toList();
}
}
React Native Layout Performance
javascript
// React Native - Flexbox optimization
import React, { useMemo } from 'react';
import { View, Dimensions, StyleSheet } from 'react-native';
const OptimizedFlexLayout = ({ children, spacing = 8 }) => {
const { width, height } = Dimensions.get('window');
// Memoize style calculations
const containerStyle = useMemo(() => ({
flex: 1,
flexDirection: width > height ? 'row' : 'column',
justifyContent: 'space-between',
alignItems: 'stretch',
padding: spacing,
}), [width, height, spacing]);
// Memoize child styles
const childStyles = useMemo(() => {
const itemCount = React.Children.count(children);
const isHorizontal = width > height;
return {
flex: 1,
marginRight: isHorizontal && spacing ? spacing : 0,
marginBottom: !isHorizontal && spacing ? spacing : 0,
};
}, [children, width, height, spacing]);
return (
<View style={containerStyle}>
{React.Children.map(children, (child, index) => (
<View key={index} style={childStyles}>
{child}
</View>
))}
</View>
);
};
// Grid layout with performance optimization
const OptimizedGrid = ({ data, numColumns, renderItem, spacing = 8 }) => {
const { width } = Dimensions.get('window');
// Calculate item dimensions
const itemWidth = useMemo(() => {
const totalSpacing = spacing * (numColumns + 1);
return (width - totalSpacing) / numColumns;
}, [width, numColumns, spacing]);
// Memoize item style
const itemStyle = useMemo(() => ({
width: itemWidth,
marginLeft: spacing,
marginBottom: spacing,
}), [itemWidth, spacing]);
// Memoize container style
const containerStyle = useMemo(() => ({
flexDirection: 'row',
flexWrap: 'wrap',
paddingTop: spacing,
justifyContent: 'flex-start',
}), [spacing]);
// Memoize rendered items
const renderedItems = useMemo(() => {
return data.map((item, index) => (
<View key={item.id || index} style={itemStyle}>
{renderItem(item, index)}
</View>
));
}, [data, renderItem, itemStyle]);
return (
<View style={containerStyle}>
{renderedItems}
</View>
);
};
// Responsive container
const ResponsiveContainer = ({ children }) => {
const [dimensions, setDimensions] = useState(Dimensions.get('window'));
useEffect(() => {
const subscription = Dimensions.addEventListener('change', ({ window }) => {
setDimensions(window);
});
return () => subscription?.remove();
}, []);
// Determine layout type
const layoutType = useMemo(() => {
const { width, height } = dimensions;
const aspectRatio = width / height;
if (width >= 1024) return 'desktop';
if (width >= 768) return 'tablet';
if (aspectRatio > 1.5) return 'landscape';
return 'portrait';
}, [dimensions]);
// Memoize responsive styles
const responsiveStyles = useMemo(() => {
switch (layoutType) {
case 'desktop':
return {
flexDirection: 'row',
padding: 24,
maxWidth: 1200,
alignSelf: 'center',
};
case 'tablet':
return {
flexDirection: 'row',
padding: 16,
};
case 'landscape':
return {
flexDirection: 'row',
padding: 12,
};
default: // portrait
return {
flexDirection: 'column',
padding: 8,
};
}
}, [layoutType]);
return (
<View style={[styles.container, responsiveStyles]}>
{children}
</View>
);
};
// Layout measurement optimization
const useDimensions = () => {
const [dimensions, setDimensions] = useState(null);
const [screenData, setScreenData] = useState(Dimensions.get('screen'));
useEffect(() => {
const subscription = Dimensions.addEventListener('change', ({ screen }) => {
setScreenData(screen);
});
return () => subscription?.remove();
}, []);
const onLayout = useCallback((event) => {
const { width, height } = event.nativeEvent.layout;
setDimensions({ width, height });
}, []);
return {
onLayout,
dimensions,
screen: screenData,
};
};
// Usage example with memoization
const OptimizedScreen = React.memo(({ data }) => {
const { onLayout, dimensions, screen } = useDimensions();
// Memoize expensive calculations
const processedData = useMemo(() => {
return data.map(item => ({
...item,
processed: true,
}));
}, [data]);
const numColumns = useMemo(() => {
if (!dimensions) return 2;
const { width } = dimensions;
if (width > 1024) return 4;
if (width > 768) return 3;
return 2;
}, [dimensions]);
return (
<View style={styles.screen} onLayout={onLayout}>
{dimensions && (
<OptimizedGrid
data={processedData}
numColumns={numColumns}
renderItem={(item) => <ItemComponent item={item} />}
/>
)}
</View>
);
});
const styles = StyleSheet.create({
container: {
flex: 1,
},
screen: {
flex: 1,
backgroundColor: '#fff',
},
});
Layout Caching and Optimization
Advanced Layout Caching
kotlin
// Android - Layout measurement caching
class CachedLayoutManager : RecyclerView.LayoutManager() {
private val measureCache = LruCache<String, CachedMeasurement>(100)
private val positionCache = LruCache<Int, CachedPosition>(200)
override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams {
return RecyclerView.LayoutParams(
RecyclerView.LayoutParams.WRAP_CONTENT,
RecyclerView.LayoutParams.WRAP_CONTENT
)
}
override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) {
if (itemCount == 0) {
removeAndRecycleAllViews(recycler)
return
}
// Use cached positions if available
for (i in 0 until itemCount) {
val cachedPosition = positionCache.get(i)
if (cachedPosition != null && !state.isPreLayout) {
layoutCachedView(i, cachedPosition, recycler)
continue
}
layoutViewWithMeasurement(i, recycler, state)
}
}
private fun layoutCachedView(position: Int, cached: CachedPosition, recycler: RecyclerView.Recycler) {
val view = recycler.getViewForPosition(position)
addView(view)
// Apply cached layout
layoutDecoratedWithMargins(
view,
cached.left,
cached.top,
cached.right,
cached.bottom
)
}
private fun layoutViewWithMeasurement(
position: Int,
recycler: RecyclerView.Recycler,
state: RecyclerView.State
) {
val view = recycler.getViewForPosition(position)
addView(view)
// Check measurement cache
val viewType = getItemViewType(view)
val cacheKey = generateCacheKey(viewType, view)
val cachedMeasurement = measureCache.get(cacheKey)
if (cachedMeasurement != null) {
// Use cached measurements
view.layout(0, 0, cachedMeasurement.width, cachedMeasurement.height)
} else {
// Measure and cache
measureChildWithMargins(view, 0, 0)
val measurement = CachedMeasurement(
view.measuredWidth,
view.measuredHeight
)
measureCache.put(cacheKey, measurement)
}
// Calculate position and cache it
val left = calculateLeft(position)
val top = calculateTop(position)
val right = left + view.measuredWidth
val bottom = top + view.measuredHeight
layoutDecoratedWithMargins(view, left, top, right, bottom)
// Cache position
positionCache.put(position, CachedPosition(left, top, right, bottom))
}
private fun generateCacheKey(viewType: Int, view: View): String {
// Include relevant view characteristics in cache key
return "${viewType}_${view.measuredWidth}_${view.measuredHeight}"
}
private fun calculateLeft(position: Int): Int {
// Implementation for calculating left position
return 0
}
private fun calculateTop(position: Int): Int {
// Implementation for calculating top position
return position * 120 // Example: fixed height items
}
data class CachedMeasurement(
val width: Int,
val height: Int
)
data class CachedPosition(
val left: Int,
val top: Int,
val right: Int,
val bottom: Int
)
}
// View pool optimization
class OptimizedRecyclerView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : RecyclerView(context, attrs, defStyleAttr) {
init {
setupOptimizedViewPool()
}
private fun setupOptimizedViewPool() {
// Increase view pool sizes for better recycling
recycledViewPool.setMaxRecycledViews(0, 20) // View type 0
recycledViewPool.setMaxRecycledViews(1, 15) // View type 1
recycledViewPool.setMaxRecycledViews(2, 10) // View type 2
// Pre-populate view pool
itemAnimator = null // Disable animations for better performance
setHasFixedSize(true) // Set if adapter changes don't change size
setItemViewCacheSize(20) // Increase cache size
}
fun prePopulateViewPool(adapter: RecyclerView.Adapter<*>) {
val tempParent = FrameLayout(context)
// Create views for each view type
for (viewType in 0 until adapter.itemCount.coerceAtMost(3)) {
repeat(5) { // Pre-create 5 views of each type
val viewHolder = adapter.createViewHolder(tempParent, viewType)
recycledViewPool.putRecycledView(viewHolder)
}
}
}
}
iOS Layout Caching
swift
// iOS - Custom layout with caching
class CachedCollectionViewLayout: UICollectionViewLayout {
private var cache: [UICollectionViewLayoutAttributes] = []
private var contentBounds = CGRect.zero
private var cachedContentSize: CGSize?
override var collectionViewContentSize: CGSize {
return cachedContentSize ?? CGSize.zero
}
override func prepare() {
super.prepare()
guard let collectionView = collectionView else { return }
// Only recalculate if cache is invalid
if cache.isEmpty || shouldInvalidateCache() {
calculateLayout()
}
}
private func calculateLayout() {
guard let collectionView = collectionView else { return }
cache.removeAll()
contentBounds = CGRect(origin: .zero, size: collectionView.bounds.size)
let itemCount = collectionView.numberOfItems(inSection: 0)
var currentX: CGFloat = 0
var currentY: CGFloat = 0
let itemSize = CGSize(width: 100, height: 100)
let spacing: CGFloat = 8
for item in 0..<itemCount {
let indexPath = IndexPath(item: item, section: 0)
// Check if we need to wrap to next row
if currentX + itemSize.width > collectionView.bounds.width {
currentX = 0
currentY += itemSize.height + spacing
}
let frame = CGRect(
x: currentX,
y: currentY,
width: itemSize.width,
height: itemSize.height
)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = frame
cache.append(attributes)
contentBounds = contentBounds.union(frame)
currentX += itemSize.width + spacing
}
cachedContentSize = contentBounds.size
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
// Return only visible attributes for performance
return cache.filter { $0.frame.intersects(rect) }
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return cache[indexPath.item]
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
guard let collectionView = collectionView else { return false }
return !newBounds.size.equalTo(collectionView.bounds.size)
}
private func shouldInvalidateCache() -> Bool {
guard let collectionView = collectionView else { return true }
// Invalidate cache if bounds changed significantly
let currentSize = collectionView.bounds.size
let cachedSize = cachedContentSize ?? .zero
return abs(currentSize.width - cachedSize.width) > 1.0 ||
abs(currentSize.height - cachedSize.height) > 1.0
}
}
// Auto Layout constraint caching
class ConstraintCache {
private var constraintCache: [String: [NSLayoutConstraint]] = [:]
func getCachedConstraints(for key: String) -> [NSLayoutConstraint]? {
return constraintCache[key]
}
func cacheConstraints(_ constraints: [NSLayoutConstraint], for key: String) {
constraintCache[key] = constraints
}
func createOrGetConstraints(
for view: UIView,
in container: UIView,
cacheKey: String
) -> [NSLayoutConstraint] {
if let cached = getCachedConstraints(for: cacheKey) {
return cached
}
let constraints = [
view.topAnchor.constraint(equalTo: container.topAnchor, constant: 8),
view.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 8),
view.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -8),
view.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: -8)
]
cacheConstraints(constraints, for: cacheKey)
return constraints
}
func clearCache() {
constraintCache.removeAll()
}
}
// Usage in view controller
class CachedLayoutViewController: UIViewController {
private let constraintCache = ConstraintCache()
override func viewDidLoad() {
super.viewDidLoad()
setupCachedLayout()
}
private func setupCachedLayout() {
let cardView = UIView()
cardView.backgroundColor = .systemBackground
cardView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(cardView)
// Use cached constraints
let constraints = constraintCache.createOrGetConstraints(
for: cardView,
in: view,
cacheKey: "main_card_constraints"
)
NSLayoutConstraint.activate(constraints)
}
}
This layout performance optimization documentation provides comprehensive techniques for improving layout efficiency across Android, iOS, Flutter, and React Native platforms, including constraint optimization, responsive design patterns, and advanced caching strategies.