5

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:

Wrong children height

Any idea how I can use the GeometryReader with a variable height for the CardView children?

Thanks in advance!

notan
  • 111
  • 9
  • Why do you need GeometryReader if Card calculates own size properly? It looks like you introduced the problem where there was none, instead of solving the real one. – Asperi Jun 13 '20 at 11:12
  • Because if you tap on a Card it will show a detailed view of that card. For that, i need the GeometryReader. I omitted that code in the example. But it basically works by having a Bool state variable showDetails and depending on that the CardView gets an Offset: `CardView(card: current).offset(y: self.showDetails ? -reader.frame(in: .global).minY : 0)`. Also the frame changes the height: `.frame(height: .showDetails ? UIScreen.main.bounds.height : 80, alignment: .center)` – notan Jun 13 '20 at 11:22
  • I also updated the original post with more code – notan Jun 13 '20 at 12:07
  • Honestly it's much harder than we think (at least for me :D) there is a reason Apple restrict text length on the preview. Especially after we introduces `GeomtryReader`, i know we need it but we need it to expand the view, so unless there is a better way to expand the view I think its pretty difficult task (again, at least for me). – Muhand Jumah Jun 14 '20 at 22:13
  • Ok, I see that the problem is in used hard-code (here & there), but I can't understand what do you try to get. Which is correct expected behaviour, look & feel? – Asperi Jun 24 '20 at 15:44
  • Ultimately, I want to recreate the expanded card view of the app store: https://imgur.com/a/1Jd4bI5. I already posted an other Stackoverflow question for this: https://stackoverflow.com/questions/62331530/animate-view-to-fullscreen-card-to-detail?noredirect=1&lq=1. Everything works except having cards with differenz sizes. – notan Jun 24 '20 at 16:36
  • With the help of @MuhandJumah i was able to come this far: [Imgur](https://imgur.com/a/ggcqyov). The heights for the image (220) and the card (300) are fixed. As soon as a title is more than one line long, it looks like this: [Imgur](https://imgur.com/a/786SZzM) – notan Jun 24 '20 at 17:18

1 Answers1

7

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).

demo

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()
    }
}
Asperi
  • 123,447
  • 8
  • 131
  • 245
  • Unbelievable, this works flawless. Thank you very much :) How and why is this ViewHeightKey needed? I don't really see how it gets its value. – notan Jun 24 '20 at 19:19
  • Just courius, do you have any improvement tips? Or a different approch to do something like this entirely? – notan Jun 25 '20 at 08:32
  • Also :D Last question i swear. Is it possible to combine it with NavigationView and TabView. Both should animate "away" on the transition to fullscreen. Just hiding them with the [Introspect](https://github.com/siteline/SwiftUI-Introspect) library works, but creates a bad animation – notan Jun 25 '20 at 11:34
  • An other question: Is it possible to to refactor the code to use a List instead of ScrollView and ForEach? – cyf Jun 30 '20 at 17:40