SwiftUI LazyVGrid Layout Issue Between iOS and macOS - ios

I'm currently working on a multiplatform app for both iOS and macOS with SwiftUI, and I'm having a layout issue with the LazyVGrid. Below is my grid implementation, and screenshots from both iOS and macOS showing the issue I'm encountering:
struct CollectionView: BaseView {
// MARK: Internal
var body: some View {
GeometryReader { geometry in
ScrollView {
LazyVGrid(
columns: gridItems(for: geometry),
alignment: .center,
spacing: 8.0
) {
ForEach(
viewModel.products,
id: \.id
) { product in
Button(action: {
debugPrint(product)
}) {
ProductCellView(product: product)
}
.buttonStyle(.borderless)
}
}
.padding(.horizontal, 8.0)
}
}
}
// MARK: Private
private func gridItems(for geometry: GeometryProxy) -> [GridItem] {
#if os(iOS)
[
GridItem(
.adaptive(
minimum: (geometry.size.width / 2.0) - 24.0
),
spacing: 8.0,
alignment: .top
)
]
#else
[
GridItem(
.adaptive(
minimum: 248.0
),
spacing: 8.0,
alignment: .top
)
]
#endif
}
}
and here is the cell view implementation
struct ProductCellView: View {
let product: Product
var body: some View {
VStack {
AsyncImage(url: product.thumbnailUrl) { image in
image
.resizable()
} placeholder: {
Image("img_placeholder")
.resizable()
}
.aspectRatio(1.0, contentMode: .fit)
HStack(alignment: .top, spacing: 4.0) {
VStack(alignment: .leading, spacing: 4.0) {
Text(product.formattedProductName)
.foregroundColor(Color("Primary Yellow"))
.font(.caption.weight(.semibold))
.multilineTextAlignment(.leading)
Text(product.author)
.foregroundColor(Color("Primary Black"))
.font(.caption2)
.multilineTextAlignment(.leading)
}
.frame(
maxWidth: .infinity,
alignment: .leading
)
// TODO: Add image button here
}
.padding(.horizontal, 8.0)
Spacer()
}
.background(Color("Primary Gray"))
}
}
And here are the two screenshots, one for iOS and one for macOS
The background and design on the iOS screenshot is the correct behavior, with the bottom of the grid item expanding down so each row is uniform in height. The macOS screenshot, however, is not exhibiting this behavior. The grid items that only have single lines of text shorter than those with two or more lines of text.
How can I make macOS exhibit the same behavior as iOS in this regard?
Thanks in advance for the help.
RK

Related

SwiftUI: Expand LazyVGrid cells to max height of each row

I'm using a LazyVGrid in a ScrollView to display cells in either a 1 (portrait) or 2 (landscape) column layout. However, the height of shorter cells in a row does not expand to match the taller cell in the same row and looks pretty terrible.
How can I ensure the height is always the same for every cell in a row? Obviously I don't want a fixed height for every cell. (To be clear, I want "Church - Eastbound" to be as tall as "Church & Market" and "West Portal" to be as tall as "Forest Hill".
ScrollView(.vertical) {
LazyVGrid(
columns: [GridItem(.adaptive(minimum: 400))],
alignment: .leading,
spacing: 16
) {
ForEach(sharedFavorites.favoriteStops.indices, id: \.self) { index in
let favorite = sharedFavorites.favoriteStops[index]
NavigationLink(
destination: SingleStationView(
station: favorite.station,
direction: favorite.direction
)
) {
BoardRow(favorite: favorite, stop: favorite.observableStop)
.padding()
.background(Color(.secondarySystemGroupedBackground))
.cornerRadius(10)
.frame(maxHeight: .infinity)
}
}
}
}
Screenshot:
I tried .frame(maxHeight: .infinity) on both the BoardRow view and the inner contents of BoardView (which is just a normal VStack). It didn't work.
You were really close — just needed to put the .frame(maxHeight:) before .background.
struct ContentView: View {
var body: some View {
ScrollView(.vertical) {
LazyVGrid(
columns: [GridItem(.adaptive(minimum: 160))],
alignment: .leading,
spacing: 16
) {
ForEach([0, 1, 2, 5, 6, 3], id: \.self) { index in
BoardCell(index: index)
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.background(Color(.secondarySystemGroupedBackground)) /// `background` goes after the frame!
.cornerRadius(10)
}
}
.padding(20)
}
.background(Color(.secondaryLabel))
}
}
/// I recreated your view for testing purposes
struct BoardCell: View {
var index: Int
var body: some View {
VStack(alignment: .leading) {
Text("Powell (Westbound)")
ForEach(0 ..< index) { _ in
HStack {
Circle()
.fill(.green)
.frame(width: 30, height: 30)
Text("4")
Text("19")
Text("17")
}
}
}
}
}
Result:

Need to create a chat bubble like Whatsapp with two labels on top of the message in SwiftUI

I'm trying to create a chat bubble like this:
Actual Bubble
Actual Bubble 2.0
This is what I have been able to achieve so far.
My attempt
My attempt
This is my code so far:
import SwiftUI
struct TestingView: View {
var body: some View {
ZStack {
/// header
VStack(alignment: .trailing) {
HStack {
HStack() {
Text("abcd")
}
HStack {
Text("~abcd")
}
}.padding([.trailing, .leading], 15)
.fixedSize(horizontal: false, vertical: true)
/// text
HStack {
Text("Hello Everyone, bdhjewbdwebdjewbfguywegfuwyefuyewvfyeuwfvwbcvuwe!")
}.padding([.leading, .trailing], 15)
/// timestamp
HStack(alignment: .center) {
Text("12:00 PM")
}.padding(.trailing,15)
}.background(Color.gray)
.padding(.leading, 15)
.frame(maxWidth: 250, alignment: .leading)
}
}
}
struct TestingView_Previews: PreviewProvider {
static var previews: some View {
TestingView()
}
}
The main goal is that I want the two labels on top to be distant relative to the size of the message content. I am not able to separate the two labels far apart i.e one should be on the leading edge of the bubble and the other one on the trailing edge.
Already tried spacer, it pushes them to the very edge, we need to apart them relative to the content size of the message as shown in attached images.
Here is a simplified code.
Regarding Spacer: To achieve your desired result you put both Text views inside of a HStack, and put a Spacer between them. So the Spacer pushes them apart to the leading and trailing edge.
Also I recommend to only use one padding on the surrounding stack.
VStack(alignment: .leading) {
// header
HStack {
Text("+123456")
.bold()
Spacer() // Spacer here!
Text("~abcd")
}
.foregroundStyle(.secondary)
// text
Text("Hello Everyone, bdhjewbdwebdjewbfguywegfuwyefuyewvfyeuwfvwbcvuwe!")
.padding(.vertical, 5)
// timestamp
Text("12:00 PM")
.frame(maxWidth: .infinity, alignment: .trailing)
}
.padding()
.background(Color.gray.opacity(0.5))
.cornerRadius(16)
.frame(maxWidth: 250, alignment: .leading)
}
We can put that header into overlay of main text, so it will be always aligned by size of related view, and then it is safe to add spacer, `cause it do not push label wider than main text.
Tested with Xcode 13.4 / iOS 15.5
var body: some View {
let padding: CGFloat = 15
ZStack {
/// header
VStack(alignment: .trailing) {
/// text
HStack {
//Text("Hello Everyone") // short test
Text("Hello Everyone, bdhjewbdwebdjewbfguywegfuwyefuyewvfyeuwfvwbcvuwe!") // long test
}
.padding(.top, padding * 2)
.overlay(
HStack { // << here !!
HStack() {
Text("abcd")
}
Spacer()
HStack {
Text("~abcd")
}
}
, alignment: .top)
.padding([.trailing, .leading], padding)
/// timestamp
HStack(alignment: .center) {
Text("12:00 PM")
}.padding(.trailing, padding)
}.background(Color.gray)
.padding(.leading, padding)
.frame(maxWidth: 250, alignment: .leading)
}
}
To separate two components with fairly space in the middle, use HStack{} with Spacer().
This is a sample approach for this case. Code is below the image:
VStack {
HStack {
Text("+92 301 8226")
.foregroundColor(.red)
Spacer()
Text("~Usman")
.foregroundColor(.gray)
}
.padding(.bottom, 5)
.padding(.horizontal, 5)
Text("Testing testingtesting testing testing testingtesting testing testing testing testing testing testing testing testing testing.")
.padding(.horizontal, 5)
HStack {
Spacer()
Text("2:57 AM")
.foregroundColor(.gray)
.font(.subheadline)
}
.padding(.trailing, 5)
}
.frame(width: 300, height: 160)
.background(.white)
.cornerRadius(15)

How to implement a draggable bar like the iPad built-in application Map's?

I'm working with an iPad application using SwiftUI. Now I have a MenuView Stacked on a MapView. The MenuView is supposed to imitate the iPad built-in app Map's menu. Now I'm puzzling about how to implement the draggable bar to adjust the height of the MenuView. I have implemented a clumsy one(code attached below), and it's also stuttered and frame-dropping. I'm wondering if there is already a ready-to-use widget in SwiftUI, because both the Map and iPadOS's floating mini-keyboard have similar things (pictures attached below). Or is there any good ways to implement it? I would appreciate it if you could give me some advice :)
My implementation:
// MenuView.swift
struct MenuView: View {
#Binding var menu_height: CGFloat
var bottom_slider: some View {
ZStack {
Rectangle()
.fill(Color.white)
.opacity(0.011)
.frame(width: 400, height: 28, alignment: .center)
.edgesIgnoringSafeArea(.all)
.padding(.top,-5)
RoundedRectangle(cornerRadius: 25)
.fill(Color.gray)
.frame(width: 100, height: 6, alignment: .center)
.padding(.bottom, 4)
.opacity(0.6)
}
}
var body: some View {
VStack{
//...
//...
//...
}
ScrollView{
//...
//...
//...
}
bottom_slider
.frame(height: 20, alignment: .center)
.gesture(DragGesture().onChanged({ value in
let a = self.menu_height + (value.location.y - value.startLocation.y)
if a<150 {
self.menu_height = 150
} else if a>750{
self.menu_height = 750
} else {
self.menu_height = a
}}))
}
}
// ContentView.swift
struct ContentView: View {
#State var munu_height : CGFloat = 400
var body: some View {
ZStack {
MyMapView(...)
.edgesIgnoringSafeArea(.all)
MenuView(menu_height: $menu_height, ...)
.padding(.horizontal, 10.0)
.frame(width: 400, height: height)
.background(BlurView(colorScheme: colorScheme))
.clipShape(RoundedRectangle(cornerRadius: 15))
.shadow(radius: 8)
.offset(x: 10, y:10)
}
}
}
My implementation pic:
Built-in Map application:
The iOS floating keyboard:

SwiftUI alignmentGuides inside background

I want to change the anchor point of the red line without affecting the whole layout.
So instead of having its horizontal anchor point .leading i want to be .center. So the center of the red line aligns with the leading of the black line.
VStack(alignment: .leading) {
Rectangle().fill(Color.Stock.gray).frame(height: 4)
.background(
GeometryReader { geo in
Rectangle().fill(Color.red).frame(width: 40, height: 8)
.alignmentGuide(HorizontalAlignment.leading) { dim in
dim[HorizontalAlignment.center]
}
},alignment: .leading)
}.padding(.horizontal, 32)
Result:
It want like this (Instead of adding it as a background i added to the VStack, but this modifies my layout to the right):
Updated: Xcode 13.4 / iOS 15.5
Simplified, no hardcode - only alignments:
FirstView()
.background(
SecondView()
.alignmentGuide(HorizontalAlignment.leading) { dim in
dim[HorizontalAlignment.center]
}
.alignmentGuide(VerticalAlignment.bottom) { dim in
dim[VerticalAlignment.top]
}
,alignment: .bottomLeading)
Test code is here
Original
Here is a demo of possible solution (with substituted color). Tested with Xcode 12 / iOS 14.
struct DemoView: View {
private let height = CGFloat(4)
var body: some View {
VStack(alignment: .leading) {
Rectangle().fill(Color.gray).frame(height: height)
.background(
VStack(alignment: .leading) {
Rectangle().fill(Color.red).frame(width: 40, height: 8)
.alignmentGuide(HorizontalAlignment.leading) { dim in
dim[HorizontalAlignment.center]
}
.alignmentGuide(VerticalAlignment.center) { dim in
dim[VerticalAlignment.top] - height / 2
}
},alignment: .leading)
}.padding(.horizontal, 32)
}
}

How to set relative width in a HStack embedded in a ForEach in SwiftUI?

I wanted to create a list (without using List view) of attributes. Each attribute is a HStack which contains two texts, name and value. I want the name text to have always 30% of the width of the whole HStack and the value text to use the rest of the horizontal space. The height of each attribute depends on the content.
I try to accomplish it by having a following view:
struct FatherList: View {
let attributes: Attributes
init(_ attributes: Attributes) {
self.attributes = attributes
}
var body: some View {
VStack(spacing: CGFloat.spacing.medium) {
ForEach(
attributes,
id: \.name,
content: ChildView.init
)
}
}
}
which contains the following ChildView:
struct ChildView: View {
let listItem: Product.Attribute
init(_ attribute: Product.Attribute) {
self.attribute = attribute
}
var body: some View {
GeometryReader { geometry in
HStack(alignment: .top, spacing: 0) {
Text(attribute.name)
.bold()
.frame(width: 0.3 * geometry.size.width)
.background(Color.yellow)
Text(attribute.value)
}
.fixedSize(horizontal: false, vertical: true)
.background(Color.red)
}
}
}
And the result I get is this:
The child views overlap which is not what I want, I want the child views to expand and follow each other. I am using geometryReader to accomplish the relative width that I described above. What am I doing wrong?
Here is a demo of possible solution. Tested with Xcode 11.4 / iOS 13.4
Note: ViewHeightKey is taken from this another my solution
struct ChildView: View {
let attribute: Attribute
#State private var fitHeight = CGFloat.zero
var body: some View {
GeometryReader { geometry in
HStack(alignment: .top, spacing: 0) {
Text(self.attribute.name)
.bold()
.frame(width: 0.3 * geometry.size.width, alignment: .leading)
.background(Color.yellow)
Text(self.attribute.value)
.fixedSize(horizontal: false, vertical: true)
.frame(width: 0.7 * geometry.size.width, alignment: .leading)
}
.background(Color.red)
.background(GeometryReader {
Color.clear.preference(key: ViewHeightKey.self,
value: $0.frame(in: .local).size.height) })
}
.onPreferenceChange(ViewHeightKey.self) { self.fitHeight = $0 }
.frame(height: fitHeight)
}
}

Resources