SwiftUI List row's not resising to fit contents - swift

I have a List View with 1) a header view, 2) dynamic ForEach views, and 3) a footer view. My issue is that the first row, in which the header view lies, won't resise to fit its contents. The code for the main view is below:
var body: some View {
List {
GeometryReader { geometry in
self.headerView(size: geometry.size)
}
ForEach(self.user.posts, id: \.self) { post in
Text(post.title)
}
Text("No more posts...")
.font(.footnote)
}
.edgesIgnoringSafeArea(.all)
}
This is the view which I am trying to achieve:
This is what I have so far...:
If it's any consolidation, the header view displays fine if it's outside of the list, however, that's not the view I'm looking for.
Thanks in advance.
P.S: Apologies for the huge images, I'm not sure how to make them appear as thumbnails...

Using GeometryReader is fine, but it should be used the proper way!
import SwiftUI
struct ContentView: View {
var body: some View {
// to get the size of view, we are going to use the width later
GeometryReader { p in
List {
GeometryReader { _p in
// put your image or whatever ...
// and set the frame width
Color.red.frame(width: p.size.width, alignment: .center)
}
// and finally fix the height !! to work as expected
.frame(height: 100)
Text("By by, World!")
}
}
}
}

I prefer to use Sections for similar purposes (sections allow to have different configuration of each), like
var body: some View {
List {
Section {
// << header view is here with own size
}
.listRowInsets(EdgeInsets()) // << to zero padding
Section { // << dynamic view is here with own settings
ForEach(self.user.posts, id: \.self) { post in
Text(post.title)
}
}
Section { // footer view is here with own size
Text("No more posts...")
.font(.footnote)
}
}
.edgesIgnoringSafeArea(.all)
}

Related

Arrange custom views in SwiftUI without space or overlap

I'm trying to build a UI (SwiftUI / iOS) out of a number of custom views.
All those custom views have a defined aspect ratio or ratio for their frame.
Here's a simplified version of such a custom view:
struct TestView: View {
var body: some View {
GeometryReader { geometry in
RoundedRectangle(cornerRadius: 20)
.foregroundColor(Color.blue)
.frame(height: geometry.size.width / 3)
}
}
}
My ContentView currently looks like that:
struct TestContentView: View {
var body: some View {
GeometryReader {geomerty in
VStack {
TestView()
TestView()
}
}
}
}
I would like to have the two rectangles to be positioned right below each other (at the top of the screen). So without any space between them. So a bit like an old-fashioned UITableView with only to rows.
But whatever I try, I only get one of two results:
They are equally spread out over the screen (vertically)
They overlap (= the view on the top only gets a vertical size of 20
The only solution I've found so far is to define the frame size of the sub-views also in the TestContentView(). But that seems to be quite un-SwiftUI.
Thanks!
Remove the GeometryReader from your content view, since it isn't doing anything
You said that your TestView has a defined aspect ratio, but, in fact, it doesn't -- it just has a defined width. If you do define an aspect ratio, it starts working as expected:
struct TestView: View {
var body: some View {
RoundedRectangle(cornerRadius: 20)
.foregroundColor(Color.blue)
.aspectRatio(3, contentMode: .fit)
}
}
struct TestContentView: View {
var body: some View {
VStack(spacing: 0) {
TestView()
TestView()
Spacer()
}
}
}

SwiftUI if embedded View

So I have an embedded view in my MainView.Swift
VStack(alignment: .leading){
MediaPlayerView()
}.frame(height: 250)
What I would like to do is tell MediaPlayerView that if its frame height is 250 then show X content.
But if no frame height then show Z content.
What is the way to do this?
If full screen mode - I want the play pause button to show.
(Note images don't show play pause button as not yet coded.)
If not in full screen mode I want it to wrap the image and now playing info into a button which can call the MediaPlayerView - and hide the play pause button.
You can use GeometryReader to determine your view's height like this:
struct MediaPlayerView: View {
var body: some View {
GeometryReader { geo in
if geo.size.height == 250 {
Text("Height 250")
} else {
Text("Height something else")
}
}
}
}
although it would probably be better to pass a parameter to MediaPlayerView telling it what you'd like to show.
Something like this could work:
struct ContentView: View {
#State private var fullView = false
var body: some View {
VStack(alignment: .leading) {
MediaPlayerView(showFullView: self.fullView)
}
.frame(height: fullView ? 500 : 250)
.onTapGesture {
self.fullView.toggle()
}
}
}
struct MediaPlayerView: View {
let showFullView: Bool
var body: some View {
VStack {
if showFullView {
Text("Full view")
} else {
Text("logo only")
}
}
}
}

SwiftUI - ScrollView has 0 width and my content is not visible

I am trying to use a scroll view for scrollable content, but whenever I nest my views inside the Scroll View, I have noticed that the views from my stacks vanish back into the view hierarchy and nothing remains visible on the screen. I have also seen that whenever I am using a ScrollView, it adds another Hosting View Controller and I don't know if this is the normal behaviour.
var body: some View {
NavigationView {
ScrollView(.vertical, showsIndicators: false) {
VStack {
ForEach(bookLibrary.indices, id: \.self) { index in
HStack {
ForEach(self.bookLibrary[index], id: \.self) { book in
BookView(book: book)
}
}
}
}
}
}
}
Getting this view hierarchy. You can also see that the HostingScrollView has a width of 0.
If you don't want to use GeometryReader just insert zero height view with correct width like this
var body: some View {
ScrollView {
VStack {
Color.clear
.frame(maxWidth: .infinity, maxHeight: 0)
ForEach(...) { each in
...
}
}
}
}
While not a perfect solution, you can use GeometryReader to set the scroll view's frame to the same width as its superview.
NavigationView {
GeometryReader {geometry in
ScrollView(.vertical) {
// TODO: Add content
}
.frame(width: geometry.size.width)
}
}
This workaround was inspired by Rob Mayoff's answer on another question.

How to create grid in SwiftUI

I know that we can create a List in vertical SwiftUI like this,
struct ContentView : View {
var body: some View {
NavigationView {
List {
Text("Hello")
}
}
}
}
but is there any way that we could split the list in 2 or 3 or maybe more spans that covers the screen like a grid like we did in UICollectionView
Checkout ZStack based example here
Grid(0...100) { _ in
Rectangle()
.foregroundColor(.blue)
}
iOS 14
There is 2 new native Views that you can use:
LazyHGrid
LazyVGrid
With code or directly from the library:
The library contains a fully working sample code that you can test yourself.
You can create your customView like this to achieve UICollectionView behavior:-
struct ContentView : View {
var body: some View {
VStack(alignment: .leading, spacing: 10) {
ScrollView(showsHorizontalIndicator: true) {
HStack {
ForEach(0...10) {_ in
GridView()
}
}
}
List {
ForEach(0...5) {_ in
ListView()
}
}
Spacer()
}
}
}
struct ListView : View {
var body: some View {
Text(/*#START_MENU_TOKEN#*/"Hello World!"/*#END_MENU_TOKEN#*/)
.color(.red)
}
}
struct GridView : View {
var body: some View {
VStack(alignment: .leading, spacing: 10) {
Image("marker")
.renderingMode(.original)
.cornerRadius(5)
.frame(height: 200)
.border(Color.red)
Text("test")
}
}
}
Available for iOS/iPadOS 14 on Xcode 12. You can use LazyVGrid to load just what the user see into screen and not the whole list, List is lazy by default.
import SwiftUI
//MARK: - Adaptive
struct ContentView: View {
var body: some View {
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum:100))]) {
ForEach(yourObjects) { object in
YourObjectView(item: object)
}
}
}
}
}
//MARK: - Custom Columns
struct ContentView: View {
var body: some View {
ScrollView {
LazyVGrid(columns: Array(repeating: GridItem(), count: 4)) {
ForEach(yourObjects) { object in
YourObjectView(item: object)
}
}
}
}
}
Don't forget replace the info objects with your info and YourObjectView with your customView.
🔴 SwiftUI’s LazyVGrid and LazyHGrid give us grid layouts with a fair amount of flexibility.
The simplest possible grid is made up of three things: your raw data, an array of GridItem describing the layout you want, and either a LazyVGrid or a LazyHGrid that brings together your data and your layout.
For example, this will create a vertical grid layout using cells that are 80 points in size:
struct ContentView: View {
let data = (1...100).map { "Item \($0)" }
let columns = [
GridItem(.adaptive(minimum: 80))
]
var body: some View {
ScrollView {
LazyVGrid(columns: columns, spacing: 20) {
ForEach(data, id: \.self) { item in
Text(item)
}
}
.padding(.horizontal)
}
.frame(maxHeight: 300)
}
}
đź”´ Using GridItem(.adaptive(minimum: 80)) means we want the grid to fit in as many items per row as possible, using a minimum size of 80 points each.
If you wanted to control the number of columns you can use .flexible() instead, which also lets you specify how big each item should be but now lets you control how many columns there are. For example, this creates five columns:
struct ContentView: View {
let data = (1...100).map { "Item \($0)" }
let columns = [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
]
var body: some View {
ScrollView {
LazyVGrid(columns: columns, spacing: 20) {
ForEach(data, id: \.self) { item in
Text(item)
}
}
.padding(.horizontal)
}
.frame(maxHeight: 300)
}
}
đź”´ A third option is to use fixed sizes. For example, this will make the first column be exactly 100 points wide, and allow the second column to fill up all the remaining space:
struct ContentView: View {
let data = (1...100).map { "Item \($0)" }
let columns = [
GridItem(.fixed(100)),
GridItem(.flexible()),
]
var body: some View {
ScrollView {
LazyVGrid(columns: columns, spacing: 20) {
ForEach(data, id: \.self) { item in
Text(item)
}
}
.padding(.horizontal)
}
.frame(maxHeight: 300)
}
}
đź”´ You can also use LazyHGrid to make a horizontally scrolling grid, which works in much the same way except it accepts rows in its initializer.
For example, we could create 10 side by side heading images that are horizontally scrolling like this:
struct ContentView: View {
let items = 1...50
let rows = [
GridItem(.fixed(50)),
GridItem(.fixed(50))
]
var body: some View {
ScrollView(.horizontal) {
LazyHGrid(rows: rows, alignment: .center) {
ForEach(items, id: \.self) { item in
Image(systemName: "\(item).circle.fill")
.font(.largeTitle)
}
}
.frame(height: 150)
}
}
}
đź”´ As you can see, the code required to create horizontal and vertical grids is almost the same, changing just rows for columns.
If you’re required to support iOS 13 you won’t have access to LazyHGrid or LazyVGrid, so read below for an alternative…
SwiftUI gives us VStack for vertical layouts and HStack for horizontal layouts, but nothing that does both – nothing that can lay out views in a grid structure.
Fortunately we can write one ourselves by leveraging SwiftUI’s view builder system. This means writing a type that must be created using a row and column count, plus a closure it can run to retrieve the views for a given cell in the grid. Inside the body it can then loop over all the rows and columns and create cells inside VStack and HStack to make a grid, each time calling the view closure to ask what should be in the cell.
In code it looks like this:
struct GridStack<Content: View>: View {
let rows: Int
let columns: Int
let content: (Int, Int) -> Content
var body: some View {
VStack {
ForEach(0 ..< rows, id: \.self) { row in
HStack {
ForEach(0 ..< columns, id: \.self) { column in
content(row, column)
}
}
}
}
}
init(rows: Int, columns: Int, #ViewBuilder content: #escaping (Int, Int) -> Content) {
self.rows = rows
self.columns = columns
self.content = content
}
}
// An example view putting GridStack into practice.
struct ContentView: View {
var body: some View {
GridStack(rows: 4, columns: 4) { row, col in
Image(systemName: "\(row * 4 + col).circle")
Text("R\(row) C\(col)")
}
}
}
That creates a 4x4 grid with an image and text in each cell.
Happy Coding ;)

Make ScrollView content fill its parent in SwiftUI

I'd love to build a scrolling screen, so I wanted to embed it ScrollView. But I am not able to achieve it the view just shrinks to its compressed size. Let's say that I want the ScrollView to scroll vertically so I'd love the content to match scrollView's width. So I use such preview:
struct ScrollSubview_Previews : PreviewProvider {
static var previews: some View {
func textfield() -> some View {
TextField(.constant("Text")).background(Color.red)
}
return Group {
textfield()
ScrollView {
textfield()
}
}
}
}
But it ends with result like this:
A simple way for you, using frame(maxWidth: .infinity)
ScrollView(.vertical) {
VStack {
ForEach(0..<100) {
Text("Item \($0)")
}
}
.frame(maxWidth: .infinity)
}
Here is a trick: introduce a HStack that only has one Spacer:
return Group {
HStack {
Spacer()
}
textfield()
ScrollView {
textfield()
}
}
Actually you don't need GeometryReader anymore. ScrollView has been refactored in Xcode beta 3. Now you can declare that you have a .horizontal or .vertical ScrollView.
This makes the ScrollView behave like it should, like any normal View protocol.
Ex:
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach((1...10).reversed()) {
AnyViewYouWant(number: $0)
}
}
}
This will result in a view with the width of its parent scrolling horizontally. The height will be defined by the ScrollView's subviews height.
It's a known issue existing also in beta 2 - check the Apple's Xcode 11 release notes here ⬇️
https://developer.apple.com/documentation/xcode_release_notes/xcode_11_beta_2_release_notes.
The workaround mentioned by Apple themselves is to set fixed frame for ScrollView inside element. In that case I suggest to use GeometryReader for fix screen width, height is automatically fit to content.
For example if you need to fit the ScrollView's content to screen width you can something like:
return GeometryReader { geometry in
Group {
self.textfield()
ScrollView {
self.textfield()
.frame(width: geometry.size.width)
}
}
}
You can also done this using extra HStack and space.
ScrollView(.vertical) {
HStack {
VStack {
ForEach(0..<100) {
Text("Item \($0)")
}
}
Spacer()
}
}
Scroll view content will expand to available size.
GeometryReader { geo in
ScrollView(.vertical) {
YourView().frame(
minWidth: geo.size.width,
minHeight: geo.size.height
)
}
}
PreviewProvider is just about your Canvas (rendering view).
if you want to place text into ScrollView you should create your View
struct MyViewWithScroll : View {
var body: some View {
ScrollView {
Text("Placeholder")
}
}
}
And after that, render your MyViewWithScroll in Canvas (window with iPhone from the right side of code editor)
#if DEBUG
struct MyViewWithScroll_Previews : PreviewProvider {
static var previews: some View {
MyViewWithScroll()
}
}
#endif