I've got a problem : I am using a ForEach loop to generate a custom view in a view.
I want them to be align like this (I've made this by creating 3 HStack inside a VStack):
The expectation
But I use ForEach, as a consequence, I am restricted to only "1 Stack".
I'm getting something like this :
The problem
Here is the code that concerns only the ScrollView :
ScrollView(.vertical, showsIndicators: true) {
HStack{
ForEach(styles, id: \.id) { style in // styles is an array that stores ids.
MusicStyleTabView(style: style, selectedBtn: self.$selected)// This view is the "cell" in question.
}
}
}
So how can I align horizontally a VStack ?
this code is going to solve your problem.
struct ContentView: View {
let gridItems = Array(repeating: GridItem(.flexible(minimum: 60)), count: 2)
var body: some View {
ScrollView {
LazyVGrid(columns: gridItems) {
ForEach(styles, id: \.id) { style in
MusicStyleTabView(style: style , selectedBtn: self.$selected)
}
}
}
}
Related
I'm building an SwiftUI app with a dropdown menu with a vertical ScrollView within another vertical ScrollView. However, the dropdown menu one (the nested one) won't scroll. I would like to give it priority somehow. It seems like a simple problem, but I have scoured the internet but cannot find an adequate solution. Here is the basic code for the problem (the code is cleaner in the app but copy and pasting particular snippets did not work very well):
ScrollView{
VStack{
(other stuff)
DropdownSelector()
(other stuff)
}
}
struct DropdownSelector(){
ScrollView{
VStack(alignment: .leading, spacing: 0) {
ForEach(self.options, id: \.self) { option in
(do things with the option)
}
}
}
Creating nested ScrollViews in the first place is probably a bad idea. Nonetheless, there is a solution.
Because with ScrollView it scrolls as much as the content height, this is a problem when they are nested. This is because the inner ScrollView isn't limited in height (because the outer ScrollView height just changes), so it acts as if it wasn't there at all.
Here is a minimal example demonstrating the problem, just for comparison:
struct ContentView: View {
var body: some View {
ScrollView {
VStack {
Text("Top view")
DropdownSelector()
Text("Bottom view")
}
}
}
}
struct DropdownSelector: View {
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 0) {
ForEach(0 ..< 10) { i in
Text("Item: \(i)")
}
}
}
}
}
To fix it, limit the height of the inner scroll view. Add this after DropdownSelector():
.frame(height: 100)
I have very simple code to testing Lazy loading Views, like in the code in down:
This is my Row:
struct RectangleView: View {
let index: Int
var body: some View {
print("rendering for: " + String(describing: index))
return Rectangle()
.frame(height: 40, alignment: .center)
.foregroundColor(.blue)
.cornerRadius(10)
.overlay(Text(String(index)).bold().foregroundColor(Color.white))
}
}
Here is the issue! if you just use LazyVStack, SwiftUI will load all the rows until last upper Range of ForEach which I do not understand this behavior! because this is LazyVStack Not VStack and I expect that LazyVStack take some consideration of laziness and not just rendering from down Range to upper Range! How ever if I just add a ScrollView to the codes! LazyVStack start working as a Lazy View, and it stops rendering Views of ForEach from down Range to upper Range!
So the question is here why LazyVStack needs ScrollView for working as a Lazy View?
I am working on a CustomScrollView and I should give Lazy Contents for my CustomScrollView and I need LazyVStack for this job, So as I said before LazyVStack works with ScrollView and since I am working on a CustomScrollView then I am not using Apple ScrollView, in this case LazyVStack acts like a normal VStack and start rendering from down Range to upper Range! Why LazyVStack is acting like this? If I do not use LazyVStack the memory usage goes upper than 100 mb and with more advanced Row it reach easy over 200 mb and it is so laggy and unusable!
How can I be sure that, I am passing Lazy Content?
struct ContentView: View {
#State private var maxRange: Int = 2_000
var body: some View {
ScrollView { // Here: If you do not use ScrollView, LazyVStack would works as VStack!!! Why?
LazyVStack {
ForEach(0...maxRange, id: \.self) { index in RectangleView(index: index) }
}
.padding()
}
}
}
This is my CustomScrollView:
struct CustomScrollView<ViewType: View>: View {
let importedContent: () -> ViewType
var body: some View { return importedContent() }
}
Use case:
struct ContentView: View {
#State private var maxRange: Int = 2_000
var body: some View {
CustomScrollView { // Here: LazyVStack would works as VStack!!! Why?
LazyVStack {
ForEach(0...maxRange, id: \.self) { index in RectangleView(index: index) }
}
.padding()
}
}
}
I have a Text() view inside a HStack inside of a ForEach inside of a VStack. The text can be a string of any length, and I have no control of that is put inside of it. The problem is that when you run the program, the views in the VStack overlap resulting in this jumbled mess
What I want to do is have a view that resizes its height based on the height of the multi line text view, so that the views never overlap, and always displays the entirety of the string.
Here is some code that generates the view in question:
struct ScrollingChatView: View {
#State var model: WatchModel
#State var messages: [DisplayableMessage] = []
var body: some View {
ScrollView {
if (!messages.isEmpty) {
LazyVStack {
ForEach(messages, id: \.sortTimestamp) { message in
CompactChatView(message: message)
}
}.padding()
} else {
Text("Getting Chat...").padding()
}
}.onReceive(model.chatDriver.publisher) { m in
self.messages = m
}
}
}
struct CompactChatView: View {
#State var message: DisplayableMessage
#State var stringMessage: String? = nil
var body: some View {
VStack(alignment: .leading) {
HStack(alignment: .top) {
Text(message.displayAuthor)
.lineLimit(1)
.layoutPriority(1)
Group {
Text(getEmojiText(message))
.font(.headline)
.fixedSize(horizontal: false, vertical: true)
}
Spacer()
Text(message.displayTimestamp)
.font(.subheadline)
.foregroundColor(Color.gray)
.layoutPriority(1)
}.padding(.all, 6.0)
}
}
func getEmojiText(_ item: DisplayableMessage) -> String {
var fullMessage: String = ""
for m in item.displayMessage {
switch m {
case .text(let s):
fullMessage += s
case .emote(_):
print()
}
}
return fullMessage
}
}
I've tried removing .fixedSize(horizontal: false, vertical: true) from the text view, but it only makes the text cut off after one line, which is not what I want.
If you need more context, the entire project in located at: https://github.com/LiveTL/apple. We're looking at code in the macOS folder.
You may find it useful to instead use a List() with a trailing closure like this...
List(itemList) { item in
Text(item)
}
This should prevent the issue you are running into when trying to display messages. For more information on lists check this out: https://developer.apple.com/documentation/swiftui/list.
You can see an example of this at 25:11 here: https://developer.apple.com/videos/play/wwdc2019/216/
I have a List containing a TextEditor
struct ContentView: View {
#State var text: String = "test"
var body: some View {
List((1...10), id: \.self) { _ in
TextEditor(text: $text)
}
}
}
But it's items are not growing on height change of the TextEditor. I have tried .fixedSize() modifier with no luck. What am I missing here?
You can use an invisible Text in a ZStack to make it dynamic.
struct ContentView: View {
#State var text: String = "test"
var body: some View {
List((1...10), id: \.self) { _ in
ZStack {
TextEditor(text: $text)
Text(text).opacity(0).padding(.all, 8) // <- This will solve the issue if it is in the same ZStack
}
}
}
}
Note that you should consider changing font size and other properties to match the TextEditor
As far as I can see from view hierarchy TextEditor is just simple wrapper around UITextView and does not have more to add, so you can huck into that layer and find UIKit solution for what you need, or ...
here is a demo of possible approach to handle it at SwiftUI level (the idea is to use Text view as a reference for wrapping behaviour and adjust TextEditor exactly to it)
Tested with Xcode 12b / iOS 14 (red border is added for better visibility)
Modified your view:
struct ContentView: View {
#State var text: String = "test"
#State private var height: CGFloat = .zero
var body: some View {
List {
ForEach((1...10), id: \.self) { _ in
ZStack(alignment: .leading) {
Text(text).foregroundColor(.clear).padding(6)
.background(GeometryReader {
Color.clear.preference(key: ViewHeightKey.self, value: $0.frame(in: .local).size.height)
})
TextEditor(text: $text)
.frame(minHeight: height)
//.border(Color.red) // << for testing
}
.onPreferenceChange(ViewHeightKey.self) { height = $0 }
}
}
}
}
Note: ViewHeightKey is a preference key, used in my other solutions, so can be get from there
ForEach and GeometryReader: variable height for children?
How to make a SwiftUI List scroll automatically?
Automatically adjustable view height based on text height 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 ;)