Issues with SwiftUI List NavigationLink frame(height: CGFloat) - ios

I am trying to display a custom view for my List Row, it is an HStack with an image, a title Text and another Text containing a short paragraph. I am having issues with the height of the NavigationLink item. I have tried setting the frame to a specific height but it does not display optimally on different devices (the small resolution difference between the iPhone XR and iPhone XS is enough to make me have to adjust the height manually).
Is there a way to have the frame height be set automatically? I have tried many things such as setting the frame height on my PlanetRow View, adding padding, setting the lineLimit to: 0, 3, 5, ... Whatever I do, if I don't set the height for the NavigationLink View, the PlanetRow extends past the NavigationLink View (visible beyond the List separators).
Here's the code for my views:
struct PlanetRow : View {
var planet: Planet
var body: some View {
HStack {
Image("Mars")
.resizable()
.frame(width: 50, height: 50)
VStack(alignment: .leading) {
Text(planet.name)
.font(.largeTitle)
Text(planet.overview)
.font(.caption)
}
.lineLimit(nil)
}
}
}
struct PlanetList : View {
var body: some View {
NavigationView {
List(planetData.identified(by: \.id)) { planet in
NavigationLink(destination: PlanetDetail(planet: planet)) {
PlanetRow(planet: planet)
}
.frame(height: 150)
}
.navigationBarTitle(Text("Planets"))
}
}
}

Related

How come ZStack is not working with device height in SwiftUI?

So I was working on a project and whenever I make a ZStack to get a background color, the subsequent VStack is basically somehow attached to the ZStack. I thought ZStack were similar to objects being stacked on top of each other.
However, the closer I make the bottomHeightMultiplier var closer to 100% the more the Text in the Vstack gets pushed off the screen. I was just trying to create a background view with the top 20% of any device screen being white and the bottom 80% of the screen being green.
Unfortunately the Text("Enter bill total") just ends up getting pushed off the screen. If I put the Spacer() below the Text, it gets pushed to the top and beyond the safe area. Putting it above the Text pushes it to bottom and beyond the safe area.
import SwiftUI
struct CalculatorScreen: View {
var screenWidth = UIScreen.main.bounds.width
var screenHeight = UIScreen.main.bounds.height
var topHeightMultiplier: CGFloat = 0.20
var bottomHeightMultiplier: CGFloat = 0.80
var body: some View {
ZStack {
VStack {
Color.white
.frame(minHeight: screenHeight*topHeightMultiplier)
Color.green
.frame(minHeight: screenHeight*bottomHeightMultiplier)
}
VStack {
Spacer()
Text("Enter bill total")
.foregroundColor(.black)
}
}
}
}
This will do the trick. What really is happening is that when using UIScreen.bounds, the safe areas create a problem, making the view longer than the screen long is. That means that the inner VStack pushes on the outer ZStack and thus makes the ZStack also longer than the device is. Thus, no the objects within the ZStack are completely independent, however the size of the parent changes when that of a child changes, implicitly affecting other child objects in the ZStack. The second VStack will be as long as the ZStack due to the Spacer(), making it look like it has been pushed off the screen.
Additionally, the VStack for the colors had a standard padding (now added spacing: 0), creating a white line between the colors, eating up space as well which is not taken into account when calculating the height of the colors. My tip is to always use colors you can see when building blocks, that way you immediately spot safe area issues and unwanted paddings.
struct CalculatorScreen: View {
var topHeightMultiplier: CGFloat = 0.20
var bottomHeightMultiplier: CGFloat = 0.80
var body: some View {
GeometryReader { geometry in
ZStack {
VStack(spacing: 0) {
Color.purple
.frame(minHeight: geometry.size.height*topHeightMultiplier)
Color.green
.frame(minHeight: geometry.size.height*bottomHeightMultiplier)
}
.ignoresSafeArea()
VStack {
Spacer()
Text("Enter bill total")
.foregroundColor(.black)
}
}
}
}
}

SwiftUI- can't drag a view outside of a ScrollView

Here is a very simplified version of my layout. I have a text view that I'm able to drag around...
struct DragView: View {
var text:String
#State var dragAmt = CGSize.zero
var body: some View {
Text(text)
.padding(15)
.background(Color.orange)
.cornerRadius(20)
.padding(5)
.frame(width: 150, height: 60, alignment: .leading)
.offset(dragAmt)
.gesture(
DragGesture(coordinateSpace: .global)
.onChanged {
self.dragAmt = CGSize(width: $0.translation.width, height: $0.translation.height)
}
.onEnded {_ in
self.dragAmt = CGSize.zero
})
}
}
And then I arrange these views like so:
struct ContentView: View {
var body: some View {
HStack {
ScrollView {
VStack {
DragView(text: "hi")
DragView(text: "hi")
DragView(text: "hi")
}
}
Divider()
VStack {
DragView(text: "hi")
DragView(text: "hi")
DragView(text: "hi")
}
}
}
}
The views that are not in the ScrollView act as expected, and I can drag them around the entire screen. The views that are in the ScrollView, however, will disappear if brought outside the bounds of the ScrollView.
Why does this occur? Is there any way to enable these views to be dragged outside the ScrollView?
Why does this occur?
You drag, you just don't see it because ScrollView clips its content view. Stacks do not, by default. If you add .clipped() for bottom VStack you'll see the same.
Is there any way to enable these views to be dragged outside the
ScrollView?
Solution might be using ZStack. Say on drag you hide one view in ScrollView and show same out-of ScrollView at the same global location, so visually it would behave as would drag original.

How to implement a horizontal table view?

I'm trying to implement a horizontal table view in swift with the following functionalities:
Images listed in a horizontal manner
Horizontal scrolling left and right
See wireframe attached.
Can someone point me in the right direction? I tried digging into UICollectionView to implement this.
- SwiftUI
This question is asked a lot and there are so many answers out there for using CollectionView. But in SwiftUI you can use a horizontal ScrollView containing a Horizontal Stack like this:
struct CardView: View {
var body: some View {
Rectangle()
.foregroundColor(.gray)
.frame(width: 100, height: 80, alignment: .center)
.cornerRadius(8)
}
}
struct ContentView: View {
var body: some View {
VStack {
HStack {
Text("Categories")
Spacer()
Button(action: {}) { Text("More") }
}.padding(16)
ScrollView(.horizontal) {
HStack(spacing: 16) {
ForEach(0...15, id: \.self) { _ in
CardView()
}
}.padding(16)
}
}
}
}
- Result
Note that you can use this inside a UITableViewCell later this month when SwiftUI officially released by Apple.

SegmentedControl breaks when using ForEach

So I am trying out SwiftUI and unfortunately it still has some issues but I dont know if its on my side or its just buggy. I am trying to use a SegmentedControl alongside a ForEach which displays images but labels of SegmentedControl is gone.
This is my ContentView:
struct ContentView : View {
#State var filter = 0
var body: some View {
NavigationView {
SegmentedControl(selection: $filter) {
Text("Tag1").tag(0)
Text("Tag2").tag(1)
}.padding(.bottom)
ForEach(1...3) { spot in
ZStack {
Image("placeholder")
.frame(height: 200)
.clipped()
Text("Swift")
.font(.largeTitle)
.foregroundColor(.white)
}.padding(.bottom, -8)
}.navigationBarTitle(Text("Explore"))
}
}
}
I dont see why it should break but it does. Am I using it wrongly or is this just a bug?
Your SegmentedControl is stretched and the reason is your image. The SegmentedControl's width is stretched to the size of the image and you're setting a custom frame to the image without making it resizable. That's why your labels are out of bounds. Just add .resizable() to your image, this will fix your image's size and your labels will also appear:
Image("placeholder")
.resizable()
.frame(height: 200)
.clipped()

ForEach inside ScrollView doesn't take whole width

I'm trying to re-create UI of my current app using SwiftUI. And it is way more difficult than I initially though.
I wanted to achieve card-like cells with some background behind them. I found that List doesn't support that, at least yet. List is so limited - it doesn't allow you to remove cell separator.
So I moved to ForEach inside ScrollView. I guess that isn't something which should be used in production for long tables but that should work for now. The problem I have is that ForeEach view doesn't take all the width ScrollView provides. I can set .frame(...) modifier but that will require hardcoding width which I definitely don't want to do.
Any ideas how to force VStack take full width of the ScrollView? I tried to use ForeEach without VStack and it has the same issue. It seems like ScrollView (parent view) "tells" its child view (VStack) that its frame is less that actual ScrollView's frame. And based on that information child views build their layout and sizes.
Here is my current result:
And here is the code:
struct LandmarkList : View {
var body: some View {
NavigationView {
ScrollView() {
VStack {
Spacer().frame(height: 160)
ForEach(landmarkData) { landmark in
LandmarkRow(landmark: landmark).padding([.leading, .trailing], 16)
}
}.scaledToFill()
.background(Color.pink)
}
.background(Color.yellow)
.edgesIgnoringSafeArea(.all)
.navigationBarTitle(Text("Landmarks"))
}
}
}
struct LandmarkRow : View {
var landmark: Landmark
var body: some View {
HStack {
VStack(alignment: .leading) {
Text(landmark.name).font(.title)
Text("Subtitle")
.font(.callout)
.color(.gray)
}
Spacer()
Text("5 mi")
.font(.largeTitle)
}.frame(height: 80)
.padding()
.background(Color.white)
.cornerRadius(16)
.clipped()
.shadow(radius: 2)
}
}
I've got the same issue, the only way I have found so far is to fix the ScrollView and the content view width, so that every subview you add inside the content view will be centered.
I created a simple wrapper that take the width as init parameter
struct CenteredList<Data: RandomAccessCollection, Content: View>: View where Data.Element: Identifiable {
public private(set) var width: Length
private var data: Data
private var contentBuilder: (Data.Element.IdentifiedValue) -> Content
init(
width: Length = UIScreen.main.bounds.width,
data: Data,
#ViewBuilder content: #escaping (Data.Element.IdentifiedValue) -> Content)
{
self.width = width
self.data = data
self.contentBuilder = content
}
var body: some View {
ScrollView {
VStack {
ForEach(data) { item in
return self.contentBuilder(item)
}.frame(width: width)
}
.frame(width: width)
}
.frame(width: width)
}
}
By default it takes the screen width (UIScreen.main.bounds.width).
It works just like a List view:
var body: some View {
TileList(data: 0...3) { index in
HStack {
Text("Hello world")
Text("#\(index)")
}
}
}
Its possible that the answer to this might just be wrapping your scrollView inside of a GeometryReader
Like done in the answer here -> How do I stretch a View to its parent frame with SwiftUI?

Resources