2

So I am trying to make this List or ScrollView in SwiftUI (not fully iOS 14 ready yet, I have to use the old api, so I can't use ScrollViewReader or other fantasy from iOS 14+ SwiftUI api). The goal is to have only a specific number of rows visible on the view and be able to center the last one.

Images always make it easier to explain. So it should look somewhat like this. enter image description here

the first two rows have a specific color applied to them, the middle one also but has to be center vertically. then there are eventually 2 more rows under but they are invisible at the moment and will be visible by scrolling down and make them appear.

The closest example i have of this, is the Apple Music Lyrics UI/UX if you are familiar with it.

I am not sure how to approach this here. I thought about create a List that will have a Text with a frame height defined by the height of the view divided by 5. But then I am not sure how to define a specific color for each row depending of which one is it in the view at the moment.

Also, It would be preferable if I can just center the selected row and let the other row have their own sizes without setting one arbitrary.

Banging my head on the walls atm, any help is welcomed! thank you guys.

Edit 1: Screenshot added as a result from @nicksarno answer. Definitely looking good. The only noted issue atm is that it highlights multiple row with the same color instead of one. enter image description here

Edit 2: I did some adjustment to the code @nicksarno published.

        let middleScreenPosition = geometryProxy.size.height / 2

        return ScrollView {
            VStack(alignment: .leading, spacing: 20) {
                Spacer()
                    .frame(height: geometryProxy.size.height * 0.4)
                ForEach(playerViewModel.flowReaderFragments, id: \.id) { text in
                    Text(text.content) // Outside of geometry ready to set the natural size
                        .opacity(0)
                        .overlay(
                            GeometryReader { geo in
                                let midY = geo.frame(in: .global).midY

                                Text(text.content) // Actual text
                                    .font(.headline)
                                    .foregroundColor( // Text color
                                        midY > (middleScreenPosition - geo.size.height / 2) && midY < (middleScreenPosition + geo.size.height / 2) ? .white :
                                            midY < (middleScreenPosition - geo.size.height / 2) ? .gray :
                                            .gray
                                    )
                                    .colorMultiply( // Animates better than .foregroundColor animation
                                        midY > (middleScreenPosition - geo.size.height/2) && midY < (middleScreenPosition + geo.size.height/2) ? .white :
                                            midY < (middleScreenPosition - geo.size.height/2) ? .gray :
                                            .clear
                                    )
                                    .animation(.easeInOut)
                            }
                        )
                }
                Spacer()
                    .frame(height: geometryProxy.size.height * 0.4)

            }
            .frame(maxWidth: .infinity)
            .padding()
        }
        .background(
            Color.black
                .edgesIgnoringSafeArea(.all)
        )

Now it is closer to what I am looking for. Also, I added some Spacer (not dynamic yet) that will allow to center the text if needed in the middle. Still need to figure how to put the text in the middle when selected one of the element.

enter image description here

5
  • For the color, i guess with my data type i can add a value like var relativePositionToMiddle: Int and set it to 0, -1, -2, +1, +2, when I set the selection. that will also allow me to somehow, center the selected view in the middle too. I dunno. I am still thinking. Commented Jan 15, 2021 at 3:19
  • i meant the data structure used for my array* Commented Jan 15, 2021 at 3:53
  • Actually, I can probably use the same relativePositionToMiddle variable to also determine all the element that are not the middle one and the two above as invisible. and then not having to manage the size of the cell anymore. just have to figure how to manage to put a specific cell in the middle. Commented Jan 15, 2021 at 3:54
  • Can you use a GeometryReader (iOS 13+)? Commented Jan 15, 2021 at 4:47
  • 1
    yeap! you can use GeomertyReader in any version of swiftui Commented Jan 15, 2021 at 5:05

1 Answer 1

1

Here's how I would do it. First, add a GeometryReader to get the size of the entire screen (screenGeometry). Using that, you can set up the boundaries for your text modifiers (upper/lower). Finally, add a GeoemteryReader behind each of the Text() and (here's the magic) you can use the .frame(in: global) function to find its current location in the global coordinate space. We can then compare the frame coordinates to our upper/lower boundaries.

A couple notes:

  • The current boundaries are at 40% and 60% of the screen.
  • I'm currently using the .midY coordinate of the Text geometry, but you can also use .minY or .maxY to get more exact.
  • The natural behavior of a GeometryReader sizes to the smallest size to fit, so to keep the natural Text sizes, I added a first Text() with 0% opacity (for the frame) and then added the GeometryReader + the actual Text() as an overlay.
  • Lastly, the .foregroundColor doesn't really animate, so I also added a .colorMultiply on top, which does animate.

Code:

 struct ScrollViewPlayground: View {
        
        let texts: [String] = [
            "So I am trying to make this List or ScrollView in SwiftUI (not fully iOS 14 ready yet, I have to use the old api, so I can't use ScrollViewReader or other fantasy from iOS 14+ SwiftUI api). The goal is to have only a specific number of rows visible on the view and be able to center the last one.",
            "Images always make it easier to explain. So it should look somewhat like this.",
            "the first two rows have a specific color applied to them, the middle one also but has to be center vertically. then there are eventually 2 more rows under but they are invisible at the moment and will be visible by scrolling down and make them appear.",
            "The closest example i have of this, is the Apple Music Lyrics UI/UX if you are familiar with it.",
            "I am not sure how to approach this here. I thought about create a List that will have a Text with a frame height defined by the height of the view divided by 5. But then I am not sure how to define a specific color for each row depending of which one is it in the view at the moment.",
            "Also, It would be preferable if I can just center the selected row and let the other row have their own sizes without setting one arbitrary.",
            "Banging my head on the walls atm, any help is welcomed! thank you guys."
        ]
        
        var body: some View {
            GeometryReader { screenGeometry in
                let lowerBoundary = screenGeometry.size.height * 0.4
                let upperBoundary = screenGeometry.size.height * (1.0 - 0.4)
                
                ScrollView {
                    VStack(alignment: .leading, spacing: 20) {
                        ForEach(texts, id: \.self) { text in
                            Text(text) // Outside of geometry ready to set the natural size
                                .opacity(0)
                                .overlay(
                                    GeometryReader { geo in
                                        let midY = geo.frame(in: .global).midY
    
                                        Text(text) // Actual text
                                            .font(.headline)
                                            .foregroundColor( // Text color
                                                midY > lowerBoundary && midY < upperBoundary ? .white :
                                                midY < lowerBoundary ? .gray :
                                                .gray
                                            )
                                            .colorMultiply( // Animates better than .foregroundColor animation
                                                midY > lowerBoundary && midY < upperBoundary ? .white :
                                                midY < lowerBoundary ? .gray :
                                                .clear
                                            )
                                            .animation(.easeInOut)
                                    }
                                )
                        }
    
                    }
                    .frame(maxWidth: .infinity)
                    .padding()
                }
                .background(
                    Color.black
                        .edgesIgnoringSafeArea(.all)
                )
            }
        }
    }
    
    struct ScrollViewPlayground_Previews: PreviewProvider {
        static var previews: some View {
            ScrollViewPlayground()
        }
    }
Sign up to request clarification or add additional context in comments.

2 Comments

Oh nice, it gets definitely closer to the goal! thanks! Little note, it seems that it highlights multiple row at the same time instead of just the middle one. I guess this is due to the bounds set via size instead of cells related. do you have a thought on that?
Even by playing with the boundaries it doesn't seem to match. It would need to be dynamic. currently, Trying to find a way Also, there are 3 colors, white, gray and dark gray, here it handles 2. Just making notes ^^

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.