SwiftUI中如何实现文本环绕ImageView布局?
SwiftUI 图文环绕布局实现方案
你当前用HStack把图片和文本拆成了两个独立的列,这就导致文本没法自动环绕到图片下方。要实现你要的那种图文混排效果,SwiftUI原生没有直接对应的API,但可以通过以下两种方法解决:
方法一:包装UITextView(推荐,适配全iOS版本)
借助UIKit的UITextView原生支持图文环绕的能力,用UIViewRepresentable包装成SwiftUI组件:
import SwiftUI import UIKit struct WrappingTextView: UIViewRepresentable { let imageURL: URL let text: String func makeUIView(context: Context) -> UITextView { let textView = UITextView() textView.isEditable = false textView.isScrollEnabled = false textView.backgroundColor = .clear // 初始化文本内容 let attributedText = NSMutableAttributedString(string: text) // 异步加载图片并插入到文本开头 URLSession.shared.dataTask(with: imageURL) { data, _, error in guard let data = data, let image = UIImage(data: data) else { return } DispatchQueue.main.async { // 把图片缩放到容器宽度的25% let imageWidth = textView.bounds.width * 0.25 let scale = imageWidth / image.size.width let resizedImage = image.resized(to: CGSize(width: imageWidth, height: image.size.height * scale)) // 创建图片附件 let attachment = NSTextAttachment() attachment.image = resizedImage attachment.bounds = CGRect(x: 0, y: -4, width: imageWidth, height: resizedImage.size.height) // 微调垂直对齐 // 把图片插入文本开头,加个空格分隔 let imageAttrString = NSAttributedString(attachment: attachment) attributedText.insert(imageAttrString, at: 0) attributedText.insert(NSAttributedString(string: " "), at: 1) // 设置文本视图的属性文本 textView.attributedText = attributedText } }.resume() return textView } func updateUIView(_ uiView: UITextView, context: Context) {} } // 给UIImage加个调整大小的扩展 extension UIImage { func resized(to size: CGSize) -> UIImage { UIGraphicsImageRenderer(size: size).image { _ in draw(in: CGRect(origin: .zero, size: size)) } } } // 使用示例 struct ContentView: View { var body: some View { WrappingTextView( imageURL: URL(string: "https://images.unsplash.com/photo-1619679505795-a4d0e6be5e02?q=80&w=1964&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D")!, text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas bibendum magna magna, eget dignissim leo lobortis eget.Maecenas bibendum magna magna, eget dignissim leo lobortis eget. Sed metus quam, condimentum in quam vel, rhoncus tempor purus. Proin tortor augue, commodo eu turpis in, rhoncus varius lorem. Morbi ut sapien eget diam viverra mattis quis et augue." ) .padding(.horizontal, 12) .padding(.vertical, 20) .border(Color.black) } }
方法二:iOS 17+ 自定义Layout(纯SwiftUI)
如果你的项目只需要适配iOS 17及以上版本,可以用SwiftUI的Layout协议自定义布局逻辑:
import SwiftUI struct ImageWrapLayout: Layout { let imageWidthRatio: CGFloat = 0.25 let spacing: CGFloat = 8 func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { guard let imageSubview = subviews.first, let textSubview = subviews.last else { return .zero } let containerWidth = proposal.width ?? 0 let imageWidth = containerWidth * imageWidthRatio let imageSize = imageSubview.sizeThatFits(.init(width: imageWidth, height: .infinity)) // 计算第一行文本的尺寸 let firstLineTextWidth = containerWidth - imageWidth - spacing let firstLineTextSize = textSubview.sizeThatFits(.init(width: firstLineTextWidth, height: .infinity)) // 剩余文本的高度 let remainingTextHeight = max(0, textSubview.sizeThatFits(proposal).height - firstLineTextSize.height) // 总高度取图片高度和第一行文本高度的最大值,加上剩余文本高度 let totalHeight = max(imageSize.height, firstLineTextSize.height) + remainingTextHeight return CGSize(width: containerWidth, height: totalHeight) } func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { guard let imageSubview = subviews.first, let textSubview = subviews.last else { return } let imageWidth = bounds.width * imageWidthRatio let imageSize = imageSubview.sizeThatFits(.init(width: imageWidth, height: .infinity)) // 放置图片在左上角 imageSubview.place( at: CGPoint(x: bounds.minX, y: bounds.minY), proposal: ProposedViewSize(width: imageWidth, height: imageSize.height) ) // 放置第一行文本在图片右侧 let firstLineTextRect = CGRect( x: bounds.minX + imageWidth + spacing, y: bounds.minY, width: bounds.width - imageWidth - spacing, height: imageSize.height ) textSubview.place( at: firstLineTextRect.origin, proposal: ProposedViewSize(width: firstLineTextRect.width, height: firstLineTextRect.height) ) // 放置剩余文本在图片下方,占满整个宽度 let remainingTextRect = CGRect( x: bounds.minX, y: bounds.minY + imageSize.height, width: bounds.width, height: bounds.height - imageSize.height ) textSubview.place( at: remainingTextRect.origin, proposal: ProposedViewSize(width: remainingTextRect.width, height: remainingTextRect.height) ) } } // 使用示例 struct ContentView: View { var body: some View { ImageWrapLayout { AsyncImage(url: URL(string: "https://images.unsplash.com/photo-1619679505795-a4d0e6be5e02?q=80&w=1964&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D")) { phase in if let image = phase.image { image .resizable() .scaledToFit() } else if phase.error != nil { Text("加载图片出错") } else { ProgressView() } } Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas bibendum magna magna, eget dignissim leo lobortis eget.Maecenas bibendum magna magna, eget dignissim leo lobortis eget. Sed metus quam, condimentum in quam vel, rhoncus tempor purus. Proin tortor augue, commodo eu turpis in, rhoncus varius lorem. Morbi ut sapien eget diam viverra mattis quis et augue.") } .padding(.horizontal, 12) .padding(.vertical, 20) .border(Color.black) } }
注意事项
- 方法一基于UIKit,兼容性好,能完美处理各种文本和图片的排版情况,是优先推荐的方案。
- 方法二是纯SwiftUI实现,但仅支持iOS 17+,且自定义Layout需要处理文本分段的逻辑,相对复杂。
内容的提问来源于stack exchange,提问作者StillAyo
相关产品推荐
相关产品推荐

