I would like to add vertical scrolling to my custom layout. I don't know how to do it. I have added my code below for reference. Thanks in Advance.
struct TokenData {
let email: String
}
let gradientColor = LinearGradient(colors: [Color(red: 0/255, green: 204/255, blue: 172/255), Color(red: 1/255, green: 99/255, blue: 240/255)], startPoint: .top, endPoint: .bottom)
struct TokenView: View {
@Binding var dataSource: [TokenData]
@Binding var tokenData: TokenData
var body: some View {
HStack {
Text(tokenData.email)
.foregroundStyle(.white)
Button(action: {
if let index = self.dataSource.firstIndex(where: { $0.email == tokenData.email }) {
self.dataSource.remove(at: index)
} else {
assertionFailure("Should not come here.. ")
}
}, label: {
Image(systemName: "multiply")
.renderingMode(.template)
})
.buttonStyle(.plain)
.foregroundStyle(.white)
}
.padding(3)
.background(gradientColor)
.clipShape(RoundedRectangle(cornerRadius: 4))
}
}
struct TokenFieldView: View {
@State var resultData: [TokenData] = []
@State var inputData: String = ""
@State var layout: AnyLayout = AnyLayout(CustomTokenFieldLayout())
var body: some View {
VStack(alignment: .center) {
HStack(alignment: .center) {
TextField("Enter mail Ids..", text: $inputData)
.onSubmit {
resultData.append(TokenData(email: inputData))
}
Button {
self.resultData.removeAll()
} label: {
Text("clear")
}
}
Divider()
ScrollView(.vertical) {
layout {
ForEach(resultData.indices, id: \.self) { index in
TokenView(dataSource: $resultData, tokenData: self.$resultData[index])
}
}
}
}
.onAppear(perform: {
let window = NSApplication.shared.windows.first
window?.level = .floating
})
}
}
struct CustomTokenFieldLayout: Layout {
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
return CGSize(width: proposal.width!, height: proposal.height!)
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
guard !subviews.isEmpty else {
return
}
var point = bounds.origin
let hSpacing: Double = 5
let vSpacing: Double = 5
point.x += hSpacing
point.y += vSpacing
for subview in subviews {
if (point.x + subview.dimensions(in: .unspecified).width + hSpacing) > bounds.width {
point.y += (subview.dimensions(in: .unspecified).height + vSpacing)
point.x = hSpacing
}
subview.place(at: point, anchor: .leading, proposal: proposal)
point.x += (subview.dimensions(in: .unspecified).width + hSpacing)
}
}
}
When I try to add my custom layout inside scrollview, I get zero proposalview.width and height in Layout definition. Could anyone please explain why that is happening?
struct TokenFieldView: View {
@State var resultData: [TokenData] = []
@State var inputData: String = ""
@State var layout: AnyLayout = AnyLayout(CustomTokenFieldLayout())
var body: some View {
VStack(alignment: .center) {
HStack(alignment: .center) {
TextField("Enter mail Ids..", text: $inputData)
.onSubmit {
resultData.append(TokenData(email: inputData))
}
Button {
self.resultData.removeAll()
} label: {
Text("clear")
}
}
Divider()
ScrollView {
layout {
ForEach(resultData.indices, id: \.self) { index in
TokenView(dataSource: $resultData, tokenData: self.$resultData[index])
}
}
}
}
.onAppear(perform: {
let window = NSApplication.shared.windows.first
window?.level = .floating
})
}
}
When I tried your example code, it crashed in CustomTokenFieldLayout.sizeThatFits
, because it was trying to unwrap an undefined optional.
Try changing the implementation of sizeThatFits
to the following:
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
proposal.replacingUnspecifiedDimensions()
}
This stops it crashing, but it is still only returning the proposal it is given, so it is not calculating the actual size that really fits. This prevents the ScrollView
from scrolling properly over the full height of its contents.
To fix, the function sizeThatfits
needs to iterate over the subviews and work out the real size that fits, as explained by the documentation.
Looking at placeSubviews
, I assume you are trying to achieve a flow layout, where as many items are shown on a row as possible. I see that you are also adding leading and trailing padding to the rows and to the overall height.
The current implementation of placeSubviews
is a bit flawed, for the following reasons:
anchor: .leading
, which means that half their height will be above the position they are placed at.Addressing these issues:
.topLeading
is used instead:subview.place(at: point, proposal: proposal)
dimensions
delivered by the subview. But in many cases, it probably works to supply the container proposal.So based on the way that placeSubviews
is working, here is an example implementation of sizeThatFits
that computes the size appropriately:
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
let hSpacing: CGFloat = 5
let vSpacing: CGFloat = 5
var rowWidth = CGFloat.zero
var rowHeight = CGFloat.zero
var totalWidth = CGFloat.zero
var totalHeight = CGFloat.zero
let horizontalPadding = hSpacing + hSpacing
for subview in subviews {
let subviewSize = subview.sizeThatFits(proposal)
let addToExistingRow: Bool
if let containerWidth = proposal.width {
let availableWidth = containerWidth - horizontalPadding
addToExistingRow = rowWidth == 0 || (rowWidth + hSpacing + subviewSize.width) <= availableWidth
} else {
addToExistingRow = true
}
if addToExistingRow {
rowWidth += rowWidth > 0 ? hSpacing : 0
rowHeight = max(rowHeight, subviewSize.height)
} else {
rowWidth = 0
if rowHeight > 0 {
totalHeight += totalHeight > 0 ? vSpacing : 0
totalHeight += rowHeight
}
rowHeight = subviewSize.height
}
rowWidth += subviewSize.width
totalWidth = max(totalWidth, rowWidth)
}
if rowWidth > 0 {
totalHeight += totalHeight > 0 ? vSpacing : 0
totalHeight += rowHeight
}
return CGSize(width: totalWidth + horizontalPadding, height: totalHeight + vSpacing + vSpacing)
}
This scrolls fine:
One other suggestion: it might be better to apply external padding to the layout
container, instead of it having internal padding at the sides and at top and bottom. This way, the size proposals that the layout receives will represent the actual space available and the layout functions do not need to be concerned with padding.