最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

swift - How to round the corners of a ScrollView without affecting indicators? - Stack Overflow

programmeradmin0浏览0评论

In my code, I want the indicators to not have a background but still be included in my UI. Therefore, I want this layout, but it has an issue—I cannot clip the ScrollView to remove those four extra parts. If I clip the ScrollView, I can get rid of the extra UI on the left side. However, since the row has padding on the right, the clipShape applies to the indicators instead of removing the unwanted extra UI. How can I maintain the UI style as it is while removing these four unnecessary parts?

import SwiftUI

struct ContentView: View {
    private let layoutMargin: CGFloat = 5.0
    private let cornerRadius: CGFloat = 5.0
    var body: some View {

        ScrollView(showsIndicators: true) {
            VStack(spacing: layoutMargin) {
                ForEach(0..<20, id: \.self) { value in
                    
                    RoundedRectangle(cornerRadius: cornerRadius)
                        .fill(Color.white)
                        .frame(height: 20.0)
                    
                }

            }
            .padding(layoutMargin)
            .padding(.trailing)

        }
        .background(
            
            Color.blue
                .clipShape(RoundedRectangle(cornerRadius: layoutMargin + cornerRadius))
                .padding(.trailing)
        
        )
        .padding()
        
    }
}

In my code, I want the indicators to not have a background but still be included in my UI. Therefore, I want this layout, but it has an issue—I cannot clip the ScrollView to remove those four extra parts. If I clip the ScrollView, I can get rid of the extra UI on the left side. However, since the row has padding on the right, the clipShape applies to the indicators instead of removing the unwanted extra UI. How can I maintain the UI style as it is while removing these four unnecessary parts?

import SwiftUI

struct ContentView: View {
    private let layoutMargin: CGFloat = 5.0
    private let cornerRadius: CGFloat = 5.0
    var body: some View {

        ScrollView(showsIndicators: true) {
            VStack(spacing: layoutMargin) {
                ForEach(0..<20, id: \.self) { value in
                    
                    RoundedRectangle(cornerRadius: cornerRadius)
                        .fill(Color.white)
                        .frame(height: 20.0)
                    
                }

            }
            .padding(layoutMargin)
            .padding(.trailing)

        }
        .background(
            
            Color.blue
                .clipShape(RoundedRectangle(cornerRadius: layoutMargin + cornerRadius))
                .padding(.trailing)
        
        )
        .padding()
        
    }
}

Share Improve this question edited Feb 3 at 10:03 Benzy Neez 23.9k3 gold badges15 silver badges45 bronze badges asked Feb 2 at 22:35 MangoMango 556 bronze badges 0
Add a comment  | 

3 Answers 3

Reset to default 1

I was expecting that the modifier .contentMargins could be used here, see contentMargins(_:_:for:). For example, something like:

ScrollView {
    // ...
}
.clipShape(.rect(cornerRadius: 20))
.contentMargins(.trailing, 20, for: .scrollIndicators)

However, on macOS, this just adds extra padding around the scroll indicator, it doesn't place the indicator inside the specified margin. So I don't think it helps.


An alternative way to clip a view is to apply a mask. This actually gives you more flexibility than .clipShape, because a mask can be a View, it is not constrained to being a Shape. So the mask can be built using an HStack that combines a rounded rectangle (for clipping the scrolled content) with a square rectangle (to avoid clipping the scroll indicators).

Actually, you could consider using a Capsule for masking the scroll indicator, this looks more natural than a rectangle with square corners.

When allowing for the scroll indicator, I think the width needs to be determined by trial and error, I don't think you can specify the width it should occupy (ref. notes above). I found that allowing a width of 14 works quite well:

ScrollView(showsIndicators: true) {
    VStack(spacing: layoutMargin) {
        // ...
    }
    .padding(layoutMargin)
}
.background {
    Color.blue
        .padding(.trailing, 14)
}
.mask {
    HStack(spacing: 0) {
        RoundedRectangle(cornerRadius: cornerRadius + layoutMargin)
        Capsule()
            .frame(width: 14)
    }
}
.padding()

I made a custom shape for the issue.

import SwiftUI

struct ContentView: View {
    private let layoutMargin: CGFloat = 5.0
    private let cornerRadius: CGFloat = 5.0
    var body: some View {

        ScrollView(showsIndicators: true) {
            VStack(spacing: layoutMargin) {
                ForEach(0..<20, id: \.self) { value in
                    
                    RoundedRectangle(cornerRadius: cornerRadius)
                        .fill(Color.white)
                        .frame(height: 20.0)
                    
                }

            }
            .padding(layoutMargin)
            .padding(.trailing, 16.0)

        }
        .background(
            
            Color.blue
                .clipShape(RoundedRectangle(cornerRadius: layoutMargin + cornerRadius))
                .padding(.trailing, 16.0)
        
        )
        .clipShape(RoundedRectWithCapsule(cornerRadius: layoutMargin + cornerRadius, capsuleWidth: 16.0))
        .padding()
        
    }
}

struct RoundedRectWithCapsule: Shape {
    var cornerRadius: CGFloat
    var capsuleWidth: CGFloat

    func path(in rect: CGRect) -> Path {
        var path = Path()
        
        let capRect = CGRect(x: rect.maxX - capsuleWidth, y: rect.minY, width: capsuleWidth, height: rect.height)
        let mainRect = CGRect(x: rect.minX, y: rect.minY, width: rect.width - capsuleWidth, height: rect.height)
        
        path.addPath(RoundedRectangle(cornerRadius: cornerRadius).path(in: mainRect))
        path.addPath(Capsule().path(in: capRect))
        
        return path
    }
}

You're encountering a common concern we tend to have when we layout our views. This is about the ordering of viewmodifiers, so you don't need .clipped() (if you tried it in hopes of clipping the white bars inside the background).

In your code, you placed .padding() after .background(), which is the reason why the UI is not what you expected it to be. Placing it like so tells the compiler to add padding to the entire view, not adding padding to the background.

For example, if your body's content is like this:

var body: some View {
        Button("Hello, world!") {
            print(type(of: self.body))
        }
        .frame(width: 200, height: 200)
        .background(.red)
}

It will create a red box with a width and height of 200. But if you change the order like so:

var body: some View {
        Button("Hello, world!") {
            print(type(of: self.body))
        }
        .background(.red)
        .frame(width: 200, height: 200)
}

You will not see a red box. Instead, you will see the red background just behind the text, yet its frame has a width and height of 200!

So, what you need to do is to place the .padding() not after .background(), but before it. This ensures that the white bars will stay inside the blue background. It will look a bit different, but I'm confident that you'll be able to solve it now that you know more about viewmodifier ordering.

If you want to know more about viewmodifier ordering: https://www.hackingwithswift/books/ios-swiftui/why-modifier-order-matters

Also, this was my code to make it closer to your expected view:

var body: some View {
        ScrollView(showsIndicators: true) {
            VStack(spacing: layoutMargin) {
                ForEach(0..<20, id: \.self) { value in
                    RoundedRectangle(cornerRadius: cornerRadius)
                        .fill(Color.white)
                        .frame(height: 20.0)
                }
            }
            .padding(.trailing)
            .padding(.trailing)
        }
        .padding(layoutMargin)
        .background(
            Color.blue
                .clipShape(RoundedRectangle(cornerRadius: layoutMargin + cornerRadius))
                .padding(.trailing)
                .padding(.trailing)
        )
    }
发布评论

评论列表(0)

  1. 暂无评论