在SwiftUI中将图像裁剪为正方形

24

我正在试图将多个单元格并排放置,其中每个单元格包含一张图像和在其下方的文本。单元格本身应该是一个正方形,图像应该缩放以填充剩余的空间(裁剪图像的一部分)。

首先,我尝试过只制作图像为正方形,然后再加上下方的文本。现在我的问题是,我不知道如何在SwiftUI中正确地实现它。当使用这段代码时,我可以使其正常工作:

VStack {
    Image(uiImage: recipe.image!)
        .resizable()
        .aspectRatio(contentMode: .fill)
        .frame(width: 200, height: 200, alignment: .center)
        .clipped()
    Text(recipe.name)
}

Image 1 问题是我必须指定一个固定的框架大小。我想要的是一种方式,可以制作一个保持1:1纵横比且可调整大小的单元格,以便我可以在屏幕上放置动态数量的它们并排显示。

我也尝试过使用

VStack {
    Image(uiImage: recipe.image!)
        .resizable()
        .aspectRatio(1.0, contentMode: .fit)
        .clipped()
    Text(recipe.name)
}

Image 2这会给我提供正方形的图片,可以动态缩放。但问题是,图像现在被拉伸以填充正方形而不是按比例缩放以填充它。

我的下一个想法是将其剪裁成正方形。为此,我首先尝试将其剪裁为圆形(因为显然没有正方形形状):

VStack {
    Image(uiImage: recipe.image!)
        .resizable()
        .aspectRatio(contentMode: .fit)
        .clipShape(Circle())
    Text(recipe.name)
}

Image 3

但出于某种奇怪的原因,它并没有真正地裁剪图片,而是保留了其余的空间...

所以是我没有看到什么还是唯一的选择是使用 frame 修改器将图像裁剪成正方形?

编辑

为了澄清: 我不太关心文本, 而是想让整个单元格 (或如果更简单, 只有图像) 是正方形, 而无需通过 .frame 指定其大小, 也无需对非正方形的原始图像进行拉伸以使其适合。

所以完美的解决方案应该是 VStack 是正方形, 但获取正方形的图像也可以。它应该看起来像 Image 1,但不需要使用 .frame 修饰符。

6个回答

34
一个ZStack将帮助我们在不影响其他视图布局的情况下分层显示视图。
对于文本内容: .frame(minWidth: 0, maxWidth: .infinity) 可以使文本水平扩展到其父视图的大小。 .frame(minHeight: 0, maxHeight: .infinity) 在其他情况下也很有用。
对于图片内容: .aspectRatio(contentMode: .fill) 可以使图片保持其宽高比而不会被压缩到其框架的大小。 .layoutPriority(-1) 可以降低图片的布局优先级,防止其扩展其父视图(在我们的情况下是ForEach中的ZStack)。 layoutPriority 的值只需要低于默认设置为0的父视图即可。我们必须这样做,因为SwiftUI会先布局子视图,然后父视图必须处理子视图的大小,除非我们手动进行不同的优先级设置。 .clipped() 修饰符使用边界框来剪切视图,因此您需要将其设置为剪切任何不是1:1宽高比的图像。
    var body: some View {
        HStack {
            ForEach(0..<3, id: \.self) { index in
                ZStack {
                    Image(systemName: "doc.plaintext")
                        .resizable()
                        .aspectRatio(contentMode: .fill)
                        .layoutPriority(-1)
                    VStack {
                        Spacer()
                        Text("yes")
                            .frame(minWidth: 0, maxWidth: .infinity)
                            .background(Color.white)
                    }
                }
                .clipped()
                .aspectRatio(1, contentMode: .fit)
                .border(Color.red)
            }
        }
    }

编辑:虽然几何读取器非常有用,但我认为尽可能避免使用它们更加清晰。让SwiftUI自己完成工作更好。这是我的初始解决方案,其中包含一个Geometry Reader,它同样有效。

        HStack {
            ForEach(0..<3, id: \.self) { index in
                ZStack {
                    GeometryReader { proxy in
                        Image(systemName: "pencil")
                            .resizable()
                            .scaledToFill()
                            .frame(width: proxy.size.width)
                        VStack {
                            Spacer()
                            Text("yes")
                                .frame(width: proxy.size.width)
                                .background(Color.white)
                        }
                    }
                }
                .clipped()
                .aspectRatio(1, contentMode: .fit)
                .border(Color.red)
            }
        }


@iComputerfreak 我更新了我的回答,以更好地回答你的问题。 - ethoooo
谢谢!GeometryReader解决方案正是我所需要的。不过,我必须将“.clipped()”移动到ZStack的末尾,否则图像会超出单元格。 - iComputerfreak
1
我先放置了 clipped(),但似乎并不重要。此外,如果您为图像的框架添加 height: proxy.size.height 参数,则图像将在垂直方向上居中。 - iComputerfreak
1
可以使用两种技术来复现该效果。 以下是可运行版本(带有饼干图像!): https://github.com/ralfebert/SwiftUIPlayground/blob/master/SwiftUIPlayground/Views/ClipImageSquareView.swift - Ralf Ebert
2
layoutPriority(-1) 做到了。非常感谢您:您刚刚为我节省了很多时间和精力。 - Konstantin Loginov
显示剩余5条评论

19

这是我在Reddit上找到并稍加改进的另一种解决方案:

Color.clear
    .aspectRatio(1, contentMode: .fit)
    .overlay(
        Image(imageName)
            .resizable()
            .scaledToFill()
        )
    .clipShape(Rectangle())

这与Chad的答案相似,但不同之处在于您将图像相对于清晰颜色(背景与覆盖层)的方式。

额外奖励:要让它具有圆形形状,只需使用.clipShape(Circle())作为最后一个修饰符。其他所有内容保持不变


3
这是对我有用的答案:
VStack {
  Color.clear
    .aspectRatio(1, contentMode: .fit)
    .background(Image(uiImage: recipe.image!)
                  .resizable()
                  .aspectRatio(contentMode: .fill))
    .clipped()
  Text(recipe.name)
}

我使用了清晰的颜色,然后使用"fit"设置了宽高比,使容器变成正方形。接着,我添加了一个背景图像,并将其设置为填充模式。最后,我添加了“clipped”属性,以防止背景图像溢出正方形的边缘。


3

基于 @ramzesenok 的答案,但封装为视图修改器。

视图修改器:

struct FitToAspectRatio: ViewModifier {

    let aspectRatio: Double
    let contentMode: SwiftUI.ContentMode

    func body(content: Content) -> some View {
        Color.clear
            .aspectRatio(aspectRatio, contentMode: .fit)
            .overlay(
                content.aspectRatio(nil, contentMode: contentMode)
            )
            .clipShape(Rectangle())
    }

}

您还可以添加扩展函数以便轻松访问

extension Image {
    func fitToAspect(_ aspectRatio: Double, contentMode: SwiftUI.ContentMode) -> some View {
        self.resizable()
            .scaledToFill()
            .modifier(FitToAspectRatio(aspectRatio: aspectRatio, contentMode: contentMode))
    }
}

然后简单地

Image(...).fitToAspect(1, contentMode: .fill)

3

对我来说可以工作,但我不知道为什么需要cornerRadius...

import SwiftUI

struct ClippedImage: View {
    let imageName: String
    let width: CGFloat
    let height: CGFloat

    init(_ imageName: String, width: CGFloat, height: CGFloat) {
        self.imageName = imageName
        self.width = width
        self.height = height
    }
    var body: some View {
        ZStack {
            Image(imageName)
                .resizable()
                .aspectRatio(contentMode: .fill)
                .frame(width: width, height: height)
        }
        .cornerRadius(0) // Necessary for working
        .frame(width: width, height: height)
    }
}

struct ClippedImage_Previews: PreviewProvider {
    static var previews: some View {
        ClippedImage("dishLarge1", width: 100, height: 100)
    }
}

enter image description here


谢谢 - 在ZStack中的Image上使用.layoutPriority(-1)对我有用。 - Morgz

0

使用GeometryReader获取视图的尺寸

 var body: some View {
    GeometryReader { geometry in
       ScrollView {
           VStack(spacing: 1) {
               ForEach(recipes) { recipe in
                   GalleryView(width: geometry.size.width, recipe: recipe)
                }
            }
        }
        .edgesIgnoringSafeArea(.all)
     }
 }

重叠手势

如果您的手势操作和图像彼此靠近,请添加.contentShape()修饰符以分配可点击区域。

宽度高度从前一个视图的GeometryReader获取。

struct GalleryView: View {

    var width: CGFloat
    var recipe: Recipe

    private enum C {
        static let goldenRatio = CGFloat(0.67)
    }

    var body: some View {
       VStack(spacing: 1) {
          Image(uiImage: recipe.image)
              .resizable()
              .scaledToFill()
              .frame(width: width, height: height, alignment: .center)
              .clipped()
              .contentShape(Rectangle())

          Text(recipe.name)
        }
        .frame(height: width * C.goldenRatio + 1)
        .frame(width: width * C.goldenRatio / 2)
      }
 }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接