I have a LazyVGrid inside a NavigationView that consists of a 2 x N grid of items. Each item contains some text of varying length and I want them to be all the same height as the biggest item.
Here's a version of my code that can be copied and pasted into a SwiftUI Playground:
import SwiftUI
import PlaygroundSupport
struct TileGridView: View {
private var items: [GridItem] {
Array(
repeating: GridItem(
.adaptive(minimum: 150),
spacing: 10
),
count: 2
)
}
var body: some View {
NavigationView {
LazyVGrid(columns: items, spacing: 10) {
TileCellView(
text: "Lorem Ipsum"
)
TileCellView(
text: "Lorem Ipsum Dolem"
)
TileCellView(
text: "Lorem ipsum dolor sit amet"
)
TileCellView(
text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit,"
)
TileCellView(
text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor"
)
}
.padding(.horizontal)
.navigationTitle("")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Image(systemName: "checkmark")
}
ToolbarItem(placement: .navigationBarTrailing) {
Image(systemName: "checkmark")
}
}
}.navigationViewStyle(.stack)
}
}
struct TileCellView: View {
#State private var isSelected: Bool = false
let text: String
var body: some View {
ZStack {
Text(text)
.padding()
.frame(maxWidth: .infinity)
.background(
RoundedRectangle(cornerRadius: 25, style: .continuous)
.foregroundColor(.blue)
)
.contentShape(Rectangle())
.onTapGesture {
isSelected = true
}
NavigationLink(
isActive: $isSelected,
destination: {
Text("hi")
},
label: {
EmptyView()
}
).hidden()
}
}
}
PlaygroundPage.current.setLiveView(TileGridView())
Now I tried to add a PreferenceKey to TileGridView to find the frame height of the biggest item but I couldn't get it working.
Update
Here's my code with a preference key involved:
struct TileGridView: View {
#State private var priceHeight: CGFloat?
private var items: [GridItem] {
Array(
repeating: GridItem(
.adaptive(minimum: 150),
spacing: 10
),
count: 2
)
}
var body: some View {
NavigationView {
LazyVGrid(columns: items, spacing: 10) {
Group {
TileCellView(
text: "Lorem Ipsum",
height: $priceHeight
)
TileCellView(
text: "Lorem Ipsum Dolem",
height: $priceHeight
)
TileCellView(
text: "Lorem ipsum dolor sit amet",
height: $priceHeight
)
TileCellView(
text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit,",
height: $priceHeight
)
TileCellView(
text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor",
height: $priceHeight
)
}
.background(
GeometryReader { geometry in
Color.clear.preference(
key: HeightPreferenceKey.self,
value: geometry.size.height
)
}
)
}
.onPreferenceChange(HeightPreferenceKey.self) {
priceHeight = $0
}
.padding(.horizontal)
.navigationTitle("")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Image(systemName: "checkmark")
}
ToolbarItem(placement: .navigationBarTrailing) {
Image(systemName: "checkmark")
}
}
}.navigationViewStyle(.stack)
}
}
private extension TileGridView {
struct HeightPreferenceKey: PreferenceKey {
static let defaultValue: CGFloat = 0
static func reduce(
value: inout CGFloat,
nextValue: () -> CGFloat
) {
value = max(value, nextValue())
}
}
}
struct TileCellView: View {
#State private var isSelected: Bool = false
let text: String
#Binding var height: CGFloat?
var body: some View {
ZStack {
Text(text)
.padding()
.frame(maxWidth: .infinity)
.frame(height: height)
.background(
RoundedRectangle(cornerRadius: 25, style: .continuous)
.foregroundColor(.blue)
)
.contentShape(Rectangle())
.onTapGesture {
isSelected = true
}
NavigationLink(
isActive: $isSelected,
destination: {
Text("hi")
},
label: {
EmptyView()
}
).hidden()
}
}
}
How can I make this work?
The issue that you are having is twofold: 1. you are reading the size of the entire LazyVGrid, not the individual cells, 2. you can't set the frame height prior to reading the cells with the PreferenceKey. What the PreferenceKey does is it reads the height that the cell wants to be before is is contained by the frame. By then taking the largest of those, and setting the frame height to that, we make them all the same size, and just large enough to contain the text. The last thing I had to do is move the blue background out of the cells as that has to be applied after the .frame(). I also cut out that part of the code that didn't relate to the LazyVGrid' or PreferenceKey` for clarity and conciseness. Your extension is unchanged. So, you get this:
struct TileGridView: View {
#State private var priceHeight: CGFloat = 50
private var items: [GridItem] {
Array(
repeating: GridItem(
.adaptive(minimum: 150),
spacing: 10
),
count: 2
)
}
var body: some View {
NavigationView {
LazyVGrid(columns: items, spacing: 10) {
Group {
TileCellView(text: "Lorem Ipsum")
.background(
GeometryReader { geometry in
Color.clear.preference(
key: HeightPreferenceKey.self,
value: geometry.size.height
)
}
)
.frame(height: priceHeight)
.background(
RoundedRectangle(cornerRadius: 25, style: .continuous)
.foregroundColor(.blue)
)
TileCellView(text: "Lorem Ipsum Dolem")
.background(
GeometryReader { geometry in
Color.clear.preference(
key: HeightPreferenceKey.self,
value: geometry.size.height
)
}
)
.frame(height: priceHeight)
.background(
RoundedRectangle(cornerRadius: 25, style: .continuous)
.foregroundColor(.blue)
)
TileCellView(text: "Lorem ipsum dolor sit amet")
.background(
GeometryReader { geometry in
Color.clear.preference(
key: HeightPreferenceKey.self,
value: geometry.size.height
)
}
)
.frame(height: priceHeight)
.background(
RoundedRectangle(cornerRadius: 25, style: .continuous)
.foregroundColor(.blue)
)
TileCellView(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit,")
.background(
GeometryReader { geometry in
Color.clear.preference(
key: HeightPreferenceKey.self,
value: geometry.size.height
)
}
)
.frame(height: priceHeight)
.background(
RoundedRectangle(cornerRadius: 25, style: .continuous)
.foregroundColor(.blue)
)
TileCellView(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor")
.background(
GeometryReader { geometry in
Color.clear.preference(
key: HeightPreferenceKey.self,
value: geometry.size.height
)
}
)
.background(
RoundedRectangle(cornerRadius: 25, style: .continuous)
.foregroundColor(.blue)
)
}
}
.onPreferenceChange(HeightPreferenceKey.self) {
priceHeight = $0
}
.padding(.horizontal)
}.navigationViewStyle(.stack)
}
}
struct TileCellView: View {
let text: String
var body: some View {
ZStack {
Text(text)
.padding()
.frame(maxWidth: .infinity)
.contentShape(Rectangle())
}
}
}
Related
from last post I decided on creating a table as following. However, when I type inside one textfield, all other textfields in the same ForEach pass through the same input. Is there something I can do, so each textfield will have its own entity?
Additionally, is this still a good solution, for when I have a few of these inside 1 view? And is this too much in terms of too many Spacers and VStacks?
How can I track all of these inputs from the textfields individually to store in coreData?
Thanks, I appreciate any type of advice.
Kind Regards
import SwiftUI
struct SwiftUIView: View {
#State var B: String = ""
#State var C: String = ""
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 22)
.fill(Color(.systemGray5))
.frame(width: 400, height: 130, alignment: .center)
VStack (alignment: .leading){
HStack {
Text("A")
.fontWeight(.bold)
Spacer()
ForEach(1..<6) { i in
Text("\(i)")
.fontWeight(.bold)
}
.frame(width: 50)
}
HStack {
Text("B")
Spacer()
ForEach(1..<6) { i in
TextField("", text: $B)
.background(
VStack {
Spacer()
Color
.primary
.frame(height: 1)
}
)
.frame(width: 50, alignment: .trailing)
}
}
HStack {
Text("C")
Spacer()
ForEach(1..<6) { i in
TextField("", text: $C)
.background(
VStack {
Spacer()
Color
.primary
.frame(height: 1)
}
)
.frame(width: 50, alignment: .trailing)
}
}
}
.frame(width: 350, height: 100, alignment: .center)
}
}
}
You are using the same variables for your TextFields within each ForEach. You need to use an array of something, and key your ForEach to the array. Here is a simplified version of your code. Notice how "B" duplicates your typing in each TextField where as "C" doesn't? The difference is that each TextField in "C" is accessing its own storage.
struct SwiftUIView: View {
#State var B: String = ""
#State var C: [DemoText] = [DemoText(value: ""), DemoText(value: "")]
var body: some View {
VStack (alignment: .leading){
HStack {
Text("B")
Spacer()
ForEach(0..<2) { _ in
TextField("", text: $B)
.background(Color.gray)
}
}
HStack {
Text("C")
Spacer()
ForEach($C) { $element in
TextField("", text: $element.value)
.background(Color.gray)
}
}
}
}
}
struct DemoText: Identifiable {
let id = UUID()
var value: String
}
newbie here!
I would like to make a onBoarding screens for launch view like in example:
I need some help please, I searched on internet but I didn't found anything which can help me with that!
I found some videos and I tried to manipulate the code from tutorial, but it didn't work.
I want to make a full View background with a VStack(alignment: .center){Text("")}.
On the internet and YouTube I found just videos which teach you slider like Sliders from WebDevelopment(header).
I would be so glad if anyone can help me! Thank you so much!
Here is another way, like this:
struct ContentView: View {
#State private var isFinished: Bool = Bool()
var body: some View {
if isFinished {
Text("Welcome!").font(Font.system(.largeTitle, design: .monospaced))
}
else {
OnboardingView(pages: pages, isFinished: { value in isFinished = value })
.statusBar(hidden: true)
}
}
}
struct OnboardingView: View {
let pages: [OnboardingPage]
var isFinished: (Bool) -> Void
#State private var currentPage: Int = 0
#State private var toggleView: Bool = Bool()
#State private var animation: Animation? = Animation.default
var body: some View {
GeometryReader { proxy in
Image(pages[currentPage].backgroundImage)
.resizable()
.ignoresSafeArea()
.scaledToFill()
VStack {
Spacer()
if toggleView {
middleVStack(index: currentPage).transition(AnyTransition.asymmetric(insertion: AnyTransition.move(edge: Edge.trailing), removal: AnyTransition.move(edge: Edge.leading)))
}
else {
middleVStack(index: currentPage).transition(AnyTransition.asymmetric(insertion: AnyTransition.move(edge: Edge.trailing), removal: AnyTransition.move(edge: Edge.leading)))
}
Spacer()
Button(action: {
if (pages.count > (currentPage + 1)) { currentPage += 1; animation = .default; toggleView.toggle() }
else { isFinished(true) }
}, label: {
Text(pages[currentPage].stringOfButton)
.font(Font.body.bold())
.padding()
.frame(maxWidth: .infinity)
.background(pages[currentPage].colorOfButton.cornerRadius(10.0))
.padding()
})
HStack {
ForEach(pages.indices, id:\.self) { index in
Circle()
.fill(Color.white.opacity(index == currentPage ? 1.0 : 0.5))
.frame(width: 12, height: 12, alignment: .center)
.onTapGesture { animation = nil ; currentPage = index }
}
}
}
.foregroundColor(.white)
.shadow(radius: 5.0)
.animation(animation, value: currentPage)
}
}
func middleVStack(index: Int) -> some View {
VStack(spacing: 20.0) {
Image(systemName: pages[index].image).font(Font.system(size: 100.0).bold())
Text(pages[index].title)
.font(Font.system(size: 50, weight: .bold, design: .rounded))
Text(pages[index].info)
.font(Font.system(.title3, design: .rounded).bold())
.padding()
}
}
}
struct OnboardingPage: Identifiable {
let id: UUID = UUID()
var backgroundImage: String
var image: String
var title: String
var info: String
var stringOfButton: String
var colorOfButton: Color
}
let info: String = " Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
var pages: [OnboardingPage] = [OnboardingPage(backgroundImage: "background1", image: "megaphone", title: "Title 1", info: info, stringOfButton: "Next", colorOfButton: .green),
OnboardingPage(backgroundImage: "background2", image: "gauge", title: "Title 2", info: info, stringOfButton: "Next", colorOfButton: .orange),
OnboardingPage(backgroundImage: "background3", image: "gyroscope", title: "Title 3", info: info, stringOfButton: "Get Started", colorOfButton: .blue)]
This is the general idea of what you are going for.
You can achieve it with a ZStack, where the image is the background. You will then want to scale the image to fit the background, ignoring safe area.
Example:
struct ContentView: View {
var body: some View {
ZStack {
Image("background")
.resizable()
.scaledToFill()
.ignoresSafeArea()
VStack {
Text("Title").font(.title)
Text("Content")
}
.foregroundColor(.white)
.shadow(radius: 1)
VStack {
Spacer()
Button {
print("pressed")
} label: {
Text("Button")
.padding()
.background(Color.yellow)
.clipShape(Capsule())
}
}
}
}
}
Result:
I do not own the rights to this image.
I have a simple login screen with two textfield and a button. It should look like this. The two textfields closer together and the button a little ways down.
Here is my code.
struct ContentView: View {
var body: some View {
VStack {
Spacer()
InputTextField(title: "First Name", text: .constant(""))
InputTextField(title: "Last Name", text: .constant(""))
Spacer()
ActionButton(title: "Login", action: {})
Spacer()
}
}
}
struct InputTextField: View {
let title: String
#Binding var text: String
var body: some View {
VStack(alignment: .leading) {
Text(title)
.foregroundColor(.primary)
.fontWeight(.medium)
.font(.system(size: 18))
HStack {
TextField("", text: $text)
.frame(height: 54)
.textFieldStyle(PlainTextFieldStyle())
.cornerRadius(10)
}
.padding([.leading, .trailing], 10)
.overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.gray, lineWidth: 0.6))
}
.padding()
}
}
struct ActionButton: View {
let title: String
var action: () -> Void
var body: some View {
Button(title) {
action()
}
.frame(minWidth: 100, idealWidth: 100, maxWidth: .infinity, minHeight: 60, idealHeight: 60)
.font(.system(size: 24, weight: .bold))
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(10)
.padding([.leading, .trailing])
.shadow(color: Color.gray, radius: 2, x: 0, y: 2)
}
}
I wanted to embed this inside a ScrollView so that user can scroll up and down when the keyboard comes up.
struct ContentView: View {
var body: some View {
ScrollView {
VStack {
Spacer()
InputTextField(title: "First Name", text: .constant(""))
InputTextField(title: "Last Name", text: .constant(""))
Spacer()
ActionButton(title: "Login", action: {})
Spacer()
}
}
}
}
Here is where I'm coming across this issue. When I add the VStack inside a ScrollView, all the content kind of shrinks and shows clumped together. Seems like the Spacers have no effect when inside a ScrollView.
How can I fix this?
Demo project
Here, You need to make the content stretch to fill the whole scroll view by giving minimum height as below
struct ContentView: View {
var body: some View {
GeometryReader { gr in
ScrollView {
VStack {
Spacer()
InputTextField(title: "First Name", text: .constant(""))
InputTextField(title: "Last Name", text: .constant(""))
Spacer()
ActionButton(title: "Login", action: {})
Spacer()
}
.frame(minHeight: gr.size.height)
}
}
}
}
Here is output:
As you have found, Spacers behave differently when they are in a ScrollView or not, or put differently, when the axis they can expand on is infinite or finite.
If what you want is for your content to be centered vertically when it fits and scroll when it's larger than the screen, I would do something like this:
struct ContentView: View {
var body: some View {
VStack { // This new stack would vertically center the content
// (I haven't actually tried it though)
ScrollView {
VStack {
Spacer().size(height: MARGIN) // The minimum margin you want
InputTextField(title: "First Name", text: .constant(""))
InputTextField(title: "Last Name", text: .constant(""))
Spacer().size(height: SPACING)
ActionButton(title: "Login", action: {})
Spacer().size(height: MARGIN)
}
}
}
}
}
I have following example:
import SwiftUI
struct TestSO: View {
#State var cards = [
Card(title: "short title text", subtitle: "short title example"),
Card(title: "medium title text text text text text", subtitle: "medium title example"),
Card(title: "long title text text text text text text text text text text text text text text text text text",
subtitle: "long title example"),
Card(title: "medium title text text text text text", subtitle: "medium title example"),
Card(title: "short title text", subtitle: "short title example"),
]
#State var showDetails = false
var body: some View {
NavigationView {
ScrollView {
VStack {
ForEach(cards.indices) { index in
GeometryReader { reader in
CardView(showDetails: self.$showDetails, card: self.cards[index])
.offset(y: self.showDetails ? -reader.frame(in: .global).minY : 0)
.onTapGesture {
self.showDetails.toggle()
self.cards[index].showDetails.toggle()
}
}.frame(height: self.showDetails ? UIScreen.main.bounds.height : 80, alignment: .center)
}
}
}.navigationBarTitle("Content", displayMode: .large)
}
}
}
struct CardView : View {
#Binding var showDetails : Bool
var card : Card
var body: some View {
VStack(alignment: .leading){
HStack{
Text(card.subtitle).padding([.horizontal, .top]).fixedSize(horizontal: false, vertical: true)
Spacer()
}
Text(card.title).fontWeight(Font.Weight.bold).padding([.horizontal, .bottom]).fixedSize(horizontal: false, vertical: true)
if(card.showDetails && showDetails) {
Spacer()
}
}
.background(Color.white)
.cornerRadius(16)
.shadow(radius: 12)
.padding()
.opacity(showDetails && card.showDetails ? 1 : (!showDetails ? 1 : 0))
}
}
struct Card : Identifiable{
var id = UUID()
var title : String
var subtitle : String
var showDetails : Bool = false
}
It's a list of cards which expand if the user taps on it. The problem here is the .frame(height: self.showDetails ? UIScreen.main.bounds.height : 80, alignment: .center) line. Depending on how much text a Card-Object has for its title or subtitle, the CardView has to be smaller or larger than 80. I need to calculate the height and use that instead of the fixed 80.
How it looks:
Any idea how I can use the GeometryReader with a variable height for the CardView children?
Thanks in advance!
Ultimately, I want to recreate the expanded card view of the app store: imgur.com/a/1Jd4bI5. I already posted an other Stackoverflow question for this: stackoverflow.com/questions/62331530/…. Everything works except having cards with differenz sizes.
Ok, I used code from that accepted post as entry point (as you said it satisfies you except different height support)
So here is a solution to support different height cells in that code using view preferences.
Tested with Xcode 12b (however I did not use SwiftUI2 features, just in case).
Only changed part:
struct ContentView: View {
#State var selectedForDetail : Post?
#State var showDetails: Bool = false
// Posts need to be #State so changes can be observed
#State var posts = [
Post(subtitle: "test1", title: "title1", extra: "Lorem ipsum dolor..."),
Post(subtitle: "test1", title: "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor", extra: "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor..."),
Post(subtitle: "test1", title: "title1", extra: "Lorem ipsum dolor..."),
Post(subtitle: "test1", title: "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis", extra: "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis..."),
Post(subtitle: "test1", title: "title1", extra: "Lorem ipsum dolor...")
]
#State private var heights = [Int: CGFloat]() // store heights in one update
var body: some View {
ScrollView {
VStack {
ForEach(self.posts.indices) { index in
GeometryReader { reader in
PostView(post: self.$posts[index], isDetailed: self.$showDetails)
.fixedSize(horizontal: false, vertical: !self.posts[index].showDetails)
.background(GeometryReader {
Color.clear
.preference(key: ViewHeightKey.self, value: $0.frame(in: .local).size.height)
})
.offset(y: self.posts[index].showDetails ? -reader.frame(in: .global).minY : 0)
.onTapGesture {
if !self.posts[index].showDetails {
self.posts[index].showDetails.toggle()
self.showDetails.toggle()
}
}
// Change this animation to what you please, or change the numbers around. It's just a preference.
.animation(.spring(response: 0.6, dampingFraction: 0.6, blendDuration: 0))
// If there is one view expanded then hide all other views that are not
.opacity(self.showDetails ? (self.posts[index].showDetails ? 1 : 0) : 1)
}
.frame(height: self.posts[index].showDetails ? UIScreen.main.bounds.height : self.heights[index], alignment: .center)
.onPreferenceChange(ViewHeightKey.self) { value in
self.heights[index] = value
}
.simultaneousGesture(
// 500 will disable ScrollView effect
DragGesture(minimumDistance: self.posts[index].showDetails ? 0 : 500)
)
}
}
}
}
}
struct ViewHeightKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue = CGFloat.zero
static func reduce(value: inout Value, nextValue: () -> Value) {
value += nextValue()
}
}
I am working on a TabView but I want to show an image that occupies the entire screen and the rest of the content in a normal way.
In vertical mode it works correctly but in horizontal mode part of the content bites the notch.
How can I correct this?
My code is as follows:
ContentView.swift
struct ContentView: View {
#State private var selection = 0
var body: some View {
TabView(selection: $selection){
Home()
.tabItem {
VStack {
Image("first")
Text("First")
}
}
.tag(0)
Text("Second View")
.font(.title)
.tabItem {
VStack {
Image("second")
Text("Second")
}
}
.tag(1)
} .edgesIgnoringSafeArea(.top)
}
}
Home.swift
struct Home: View {
var body: some View {
GeometryReader { geometry in
ZStack {
Color(.red)
.edgesIgnoringSafeArea(.all)
ScrollView {
VStack {
Image("slider_concierto1")
.resizable()
//.edgesIgnoringSafeArea(.all)
.frame(height: 250)
.aspectRatio(contentMode: .fit)
VStack {
Text("Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.")
}
.padding()
//.frame(minWidth: 0, maxWidth: .infinity, alignment: Alignment.topLeading)
.background(RoundedRectangle(cornerRadius: 16) .foregroundColor(Color.green))
.padding()
}
.frame(width: geometry.size.width)
} //scrollview
} //GEO
}//geo
.edgesIgnoringSafeArea(.all)
}
}