How to animate collapsing for a Text in SwiftUI - ios

I need help with SwiftUI animation/transition behavior. I have to implement collapsing/expanding Text during changing lineLimit from nil to 5 and vice versa.
At the moment I have a working code, you can see it below.
import SwiftUI
public struct ContentView: View {
private let description: String
#State private var isCollapsed: Bool = true
#State private var isExpandeButtonShow: Bool = false
public init(description: String) {
self.description = description
}
// MARK: - Body
public var body: some View {
VStack(alignment: .leading, spacing: 0) {
text.padding(.top, 26)
.font(Font.system(size: 12))
if isExpandeButtonShow {
collapseButton.padding(.top, 9)
}
Spacer()
}
.padding(.horizontal, 16)
}
// MARK: - Private
private var text: some View {
Text(description)
.lineLimit(isCollapsed ? 5 : nil)
.background(
GeometryReader { geometry in
Color.clear.onAppear {
truncateIfNeeded(withGeometry: geometry)
}
}
)
.animation(.linear(duration: 4))
.transition(.opacity)
}
private func truncateIfNeeded(withGeometry geometry: GeometryProxy) {
let total = description.boundingRect(
with: CGSize(
width: geometry.size.width,
height: .greatestFiniteMagnitude
),
options: .usesLineFragmentOrigin,
attributes: [.font: UIFont.systemFont(ofSize: 12)],
context: nil
)
if total.size.height > geometry.size.height {
isExpandeButtonShow = true
}
}
private var collapseButton: some View {
button(title: collapseButtonTitle()) {
isCollapsed.toggle()
}
}
private func button(title: String, handler: #escaping () -> Void) -> some View {
Button(action: handler) {
Text(title)
.foregroundColor(Color.blue)
.contentShape(Rectangle())
}
.buttonStyle(PlainButtonStyle())
}
private func collapseButtonTitle() -> String {
isCollapsed ? "Collapse" : "Expand"
}
}
It almost does what I want. But there are two points of my pain from this behavior.
First, when I try to collapse text, no animation/transition starts. It just collapses immediately. Secondly, I want to have a line appearing by line with animating each one from 0 opacity to 1. How can I do it?
Any thoughts can be helpful. Regards.

Is something like this what you're trying to do?
struct RandomView: View {
#State var isCollapsed: Bool = false
#State var lineCount: Int = 1
#State var isAnimating: Bool = false
var body: some View {
Text("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.")
.font(.largeTitle)
.lineLimit(lineCount)
.animation(.linear(duration: 1.0))
.onTapGesture {
animateLines()
}
}
func animateLines() {
guard !isAnimating else { return } // Check not currently animating
isCollapsed.toggle()
isAnimating = true
let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { (timer) in
if isCollapsed {
//Expand
lineCount += 1
if lineCount >= 10 { // max lines
timer.invalidate()
isAnimating = false
}
} else {
//Collapse
lineCount -= 1
if lineCount <= 1 { // max lines
timer.invalidate()
isAnimating = false
}
}
}
timer.fire()
}
}
EDIT: If you want to animate opacity line by line, you need to break the String into different Text items, which each have individual opacities. Like this:
var body: some View {
VStack {
Text("Line 1")
.opacity(lineCount >= 1 ? 1 : 0)
Text("Line 2")
.opacity(lineCount >= 2 ? 1 : 0)
Text("Line 3")
.opacity(lineCount >= 3 ? 1 : 0)
Text("Line 4")
.opacity(lineCount >= 4 ? 1 : 0)
Text("Line 5")
.opacity(lineCount >= 5 ? 1 : 0)
}
.font(.largeTitle)
.animation(Animation.linear(duration: 1.0))
.onTapGesture {
animateLines()
}
}

Related

How to make LazyVGrid GridItems all the same height?

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())
}
}
}

VStack extends its content layout. Is it possible to change a VStack's distribution?

In a nutshell, I'm trying to achieve the layout which on the left of the screenshot, but VStack extends its content (the right layout). Is it any way to adjust the distribution of VStack with the desired layout?
struct View2: View {
var body: some View {
VStack(spacing: 0) {
SingleView(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin lobortis tempor mi ultrices")
SingleView(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin lobortis tempor mi ultrices")
SingleView(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin lobortis tempor mi ultrices")
}
}
}
private struct SingleView: View {
let text: String
var body: some View {
HStack(alignment: .center) {
VStack(alignment: .center, spacing: 4) {
Capsule()
.cornerRadius(2)
.frame(width: 2)
.foregroundColor(.graphic)
Circle()
.strokeBorder(Color.graphic, lineWidth: 2)
.frame(width: 12, height: 12)
Capsule()
.cornerRadius(2)
.frame(width: 2)
.foregroundColor(.graphic)
}
Text(text)
.body()
}
}
}
Here is another way for solving the issue:
struct View2: View {
var body: some View {
VStack(spacing: 0.0) {
HStack {
Spacer()
Button(action: {}, label: { Image(systemName: "trash").foregroundColor(.red) }).padding(.horizontal)
Button(action: {}, label: { Image(systemName: "highlighter") })
}
.padding(10)
SingleView(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin lobortis tempor mi ultrices")
SingleView(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin lobortis tempor mi ultrices")
SingleView(text: " 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.")
SingleView(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin lobortis tempor mi ultrices")
}
.padding()
.background(Color.gray.opacity(0.2).cornerRadius(15))
.padding()
}
}
private struct SingleView: View {
let text: String
var body: some View {
HStack { Text(text).padding(.leading, 24).padding(.bottom, 24); Spacer() }
.overlay(
VStack(alignment: .center, spacing: 0.0) {
Circle()
.strokeBorder(Color.gray, lineWidth: 2)
.frame(width: 12, height: 12)
.padding(4)
Capsule()
.frame(width: 2)
}
, alignment: .leading)
}
}
You can set the maxHeight for the Capsules.
private struct SingleView: View {
let text: String
let maxHeight: CGFloat = 50 // <- HERE
var body: some View {
HStack(alignment: .center) {
VStack(alignment: .center, spacing: 4) {
Capsule()
.cornerRadius(2)
.frame(width: 2)
.frame(maxHeight: maxHeight) // <- HERE
.foregroundColor(.gray)
Circle()
.strokeBorder(Color.gray, lineWidth: 2)
.frame(width: 12, height: 12)
Capsule()
.cornerRadius(2)
.frame(width: 2)
.frame(maxHeight: maxHeight) // <- HERE
.foregroundColor(.gray)
}
Text(text)
}
}
}
Result:
Bonus
The Capsule()s don't need to be given cornerRadius(_:).
To avoid the slight gaps between each SingleView because of the rounded capsules, set View2's VStack spacing to -2 (negative of capsule widths).

OnBoarding screen with background full image - SwiftUI

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.

Resize views imported from another view - SwiftUI

I am trying to lay out a view with data from my data model and another view but I am struggling to resize the imported view. As you can see from the image below the table I have made in a separate view is taking over the full-frame.
I have tried to resize it in my master view where I am pulling everything together but no luck. I have also tried the scaleEffect modifier but that only scales the content in the view and keeps the view bounding regions the same scale.
Ideally, I would like to resize the table down so there is an even split between the paragraph and table. Also, apologies for the large amount of code shared here but I want to share all the formatting I have used incase there is something in there that is stopping me get the desired result.
The below image is displayed on an iPad which is where the app is being distributed.
Here is where I am pulling all the data together:
struct Tab1Section08: View {
var product: ProductModel
let frameHeight: CGFloat = 900
let title = "This is a title"
let subtitle = "This is a subtitle"
let paragraph = "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."
struct Tab1Section08: View {
let frameHeight: CGFloat = 900
let title = "This is a title"
let subtitle = "This is a subtitle"
let paragraph = "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 body: some View {
ZStack{
Color(red: 0.97, green: 0.97, blue: 0.97)
.ignoresSafeArea()
.frame(height: frameHeight)
VStack {
Text(title)
.font(.custom("Avenir-Black", size: 30))
.padding(.top, 60)
.padding(.bottom, 20)
HStack{
VStack {
Text(subtitle)
.font(.custom("Avenir-Black", size: 20))
Text(paragraph)
.font(.custom("Avenir", size: 16))
.multilineTextAlignment(.center)
.fixedSize(horizontal: false, vertical: true)
.lineSpacing(10)
.padding(.bottom, 20)
}
VStack {
Tab1Section08ListRow()
.padding(.bottom, 20)
Text(subtitle)
.font(.custom("Avenir", size: 12))
}
}
}
}
}
}
Here is a view where I am creating the table:
struct Tab1Section08ListRow: View {
let color: Color = .gray
let width: CGFloat = 1
let textPadding: CGFloat = 5
let frameWidth: CGFloat = 250
let amount: CGFloat = 3
let title = ["first", "second","third"]
let columns = ["first", "second", "third", "fourth", "fifth", "sixth"]
var body: some View {
let height: CGFloat = CGFloat(title.count)*40
VStack {
HStack{
ForEach(0..<title.count, id: \.self) { item in
Text(title[item])
.font(.custom("Avenir-Heavy", size: 18))
.fontWeight(.bold)
.frame(width: frameWidth)
.padding(.bottom, 20)
}
}
HStack{
VStack(alignment: .leading) {
ForEach(0..<columns.count, id: \.self) { item in
Text(columns[item])
.font(.custom("Avenir", size: 14))
.foregroundColor(.secondary)
.frame(width: frameWidth)
.padding(.bottom, textPadding)
.padding(.top, textPadding)
}
}
Rectangle()
.fill(color)
.frame(width: width, height: height)
.edgesIgnoringSafeArea(.vertical)
VStack(alignment: .leading) {
ForEach(0..<columns.count, id: \.self) { item in
Text(columns[item])
.font(.custom("Avenir", size: 14))
.foregroundColor(.secondary)
.frame(width: frameWidth)
.padding(.bottom, textPadding)
.padding(.top, textPadding)
}
}
Rectangle()
.fill(color)
.frame(width: width, height: height)
.edgesIgnoringSafeArea(.vertical)
VStack(alignment: .leading) {
ForEach(0..<columns.count, id: \.self) { item in
Text(columns[item])
.font(.custom("Avenir", size: 14))
.foregroundColor(.secondary)
.frame(width: frameWidth)
.padding(.bottom, textPadding)
.padding(.top, textPadding)
}
}
}
}
}
}
As I suspected from the beginning it is your fixed frames. If I comment out .frame(width: frameWidth) the view collapses to give your "lorem ipsum" view more room.
With .frame(width: frameWidth):
And without:
Hard coding frames like that messes with SwiftUI's ability to handle the different sizes. I would consider using a geometry reader and expressing the widths a percentage of screen size.

ForEach and GeometryReader: variable height for children?

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()
}
}

Resources