Created
May 12, 2023 18:46
-
-
Save satishVekariya/bfdb56192f6d61a7b43636b17eec06a9 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// LastAtBottomVStack.swift | |
// | |
// | |
// Created by Satish Vekariya on 21/04/2023. | |
// | |
import SwiftUI | |
// MARK: - LastAtBottomVStack | |
/// A view that arranges its subviews in a vertical line and place last view at bottom or at last position based on given `viewPortSize`. | |
/// | |
/// Recommendation: Embed this layout inside `GeometryReader` & `ScrollView` like: | |
/// | |
/// ``` | |
/// GeometryReader { geo in | |
/// ScrollView { | |
/// LastAtBottomVStack(viewPortSize: geo.size) { | |
/// Rectangle().fill(.red) | |
/// .frame(height: 150) | |
/// Rectangle().fill(.blue) | |
/// .frame(height: 130) | |
/// Rectangle().fill(.gray) /// Will stay at bottom or last | |
/// .frame(height: 200) | |
/// } | |
/// } | |
/// } | |
/// ``` | |
/// | |
public struct LastAtBottomVStack: Layout { | |
public let viewPortSize: CGSize | |
public init(viewPortSize: CGSize) { | |
self.viewPortSize = viewPortSize | |
} | |
public func sizeThatFits( | |
proposal: ProposedViewSize, | |
subviews: Subviews, | |
cache: inout () | |
) | |
-> CGSize { | |
.init( | |
/// View's width | |
width: viewPortSize.width, | |
/// Calculate total height required for subviews and pick max value from total heigh and view port height | |
height: max(viewPortSize.height, subviews.reduce(0) { $0 + $1.dimensions(in: proposal).height }) | |
) | |
} | |
public func placeSubviews( | |
in bounds: CGRect, | |
proposal: ProposedViewSize, | |
subviews: Subviews, | |
cache: inout () | |
) { | |
var occupiedHeight = bounds.origin.y // Set initial value | |
for view in subviews { | |
/// This view height | |
let thisViewHeight = view.dimensions(in: proposal).height | |
/// Check, can place view at bottom within bound | |
let canPlaceLastViewAtBottom = thisViewHeight + occupiedHeight >= bounds.height | |
let yPosition: CGFloat | |
if subviews.count > 1 && view == subviews.last && !canPlaceLastViewAtBottom { | |
/// Set bottom position within bounds | |
yPosition = bounds.height - thisViewHeight | |
} else { | |
/// Set last position irrespective to bounds | |
yPosition = occupiedHeight | |
} | |
/// Set view's position | |
view.place(at: .init(x: bounds.midX, y: yPosition), anchor: .top, proposal: proposal) | |
/// Increment `occupiedHeight` value | |
occupiedHeight += thisViewHeight | |
} | |
} | |
} | |
// MARK: - LastAtBottomVStack_Previews | |
struct LastAtBottomVStack_Previews: PreviewProvider { | |
static var previews: some View { | |
GeometryReader { geo in | |
ScrollView { | |
LastAtBottomVStack(viewPortSize: geo.size) { | |
Rectangle().fill(.red) | |
.frame(height: 150) | |
Rectangle().fill(.blue) | |
.frame(height: 130) | |
Rectangle().fill(.gray) /// Will stay at bottom or last | |
.frame(height: 200) | |
} | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment