SwiftUI布局意外依赖Metal Texture2DDescriptor,如何实现双视图等宽?
问题描述
我有一个SwiftUI视图,本应在HStack中显示两个等宽的视图。我的ContentView代码如下:
struct ContentView: View { var body: some View { HStack { Group { View1() View1() } .frame(maxWidth: .infinity) .border(.black) } .padding() .frame(maxWidth: .infinity) } }
此时两个视图能正常保持等宽。
但当把第二个视图换成基于Metal纹理的View2后,布局出现异常。View2通过Metal纹理初始化,将其转换为CGImage后用于创建Image组件,代码如下:
struct View2: View { let texture: MTLTexture init() { let device = MTLCreateSystemDefaultDevice()! let textureDescriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: MTLPixelFormat.rgba8Unorm, width: Int(700), height: Int(100), mipmapped: false) texture = device.makeTexture(descriptor: textureDescriptor)! } func textureToCGImage(texture: MTLTexture) -> CGImage? { let width = texture.width let height = texture.height let rowBytes = width * 4 let length = rowBytes * height var bgraBytes = [UInt8](repeating: 0, count: length) let region = MTLRegionMake2D(0, 0, width, height) texture.getBytes(&bgraBytes, bytesPerRow: rowBytes, from: region, mipmapLevel: 0) guard let providerRef = CGDataProvider(data: NSData(bytes: &bgraBytes, length: length)) else { return nil } let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue) let colorSpace = CGColorSpaceCreateDeviceRGB() return CGImage(width: width, height: height, bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: rowBytes, space: colorSpace, bitmapInfo: bitmapInfo, provider: providerRef, decode: nil, shouldInterpolate: false, intent: .defaultIntent) } var body: some View { let image = textureToCGImage(texture: texture)! VStack { Image(image, scale: 1, orientation: .up, label: Text("View2")) .aspectRatio(1.0, contentMode: .fit) Text("View2") } } }
替换后,View2的宽度被Metal纹理的原始宽度(700px)影响,导致两个视图不再等宽。我不清楚为何SwiftUI布局系统会依赖MTLTextureDescriptor的宽度,请问如何让两个视图始终保持相同宽度,且不受设备尺寸影响?
解决方案
核心原因
SwiftUI布局系统会依据视图的固有尺寸分配空间。View2中的Image基于700px宽的CGImage创建,即便设置了.aspectRatio(1.0, contentMode: .fit),它的固有宽度仍会被原始CGImage的尺寸左右,使得View2的整体固有宽度大于View1,最终破坏HStack的等宽布局。
具体解决方法
方法1:强制子视图填充可用空间
给View2的根容器添加.frame(maxWidth: .infinity),让它忽略自身固有尺寸,填充HStack分配的空间。
修改View2的body:
var body: some View { let image = textureToCGImage(texture: texture)! VStack { Image(image, scale: 1, orientation: .up, label: Text("View2")) .aspectRatio(1.0, contentMode: .fit) Text("View2") } .frame(maxWidth: .infinity) // 强制填充宽度 }
ContentView调整为直接给每个子视图设置填充:
struct ContentView: View { var body: some View { HStack { View1() .frame(maxWidth: .infinity) .border(.black) View2() .frame(maxWidth: .infinity) .border(.black) } .padding() .frame(maxWidth: .infinity) } }
这种方式符合SwiftUI的布局逻辑,让视图自动适应分配的空间。
方法2:用GeometryReader手动分配宽度
通过GeometryReader获取HStack的可用宽度,手动将每个子视图的宽度设为总宽度的一半:
struct ContentView: View { var body: some View { GeometryReader { geo in HStack(spacing: 8) { // 可自定义间距 View1() .frame(width: (geo.size.width - 8) / 2) // 减去间距后平分 .border(.black) View2() .frame(width: (geo.size.width - 8) / 2) .border(.black) } } .padding() .frame(maxWidth: .infinity) } }
方法3:使用Grid布局(iOS 16+)
如果应用支持iOS 16及以上,使用Grid可以更简洁地实现等宽布局:
struct ContentView: View { var body: some View { Grid(horizontalSpacing: 8) { GridRow { View1() .border(.black) View2() .border(.black) } } .gridStyle(.columns(2)) // 强制分为2列,每列等宽 .padding() .frame(maxWidth: .infinity) } }
性能优化建议
避免在View2的body中调用textureToCGImage,因为body会被频繁计算,每次都会重新转换纹理,影响性能。建议提前在init中完成转换:
struct View2: View { let cgImage: CGImage init() { let device = MTLCreateSystemDefaultDevice()! let textureDescriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: MTLPixelFormat.rgba8Unorm, width: Int(700), height: Int(100), mipmapped: false) let texture = device.makeTexture(descriptor: textureDescriptor)! self.cgImage = textureToCGImage(texture: texture)! } func textureToCGImage(texture: MTLTexture) -> CGImage? { // 方法实现保持不变 } var body: some View { VStack { Image(cgImage, scale: 1, orientation: .up, label: Text("View2")) .aspectRatio(1.0, contentMode: .fit) Text("View2") } .frame(maxWidth: .infinity) } }
内容的提问来源于stack exchange,提问作者Reinhard Männer

