返回

SwiftUI ARView 大小与对齐难题:深度解析与实战方案

IOS

SwiftUI ARView 大小与对齐问题

在 SwiftUI 中使用 ARKit 的 ARView 时,调整视图的大小和对齐方式有时会变得比较棘手。 当需要将 ARView 以非全屏方式展示时,这些问题可能会尤为明显。下面深入分析这种常见问题的原因,并给出解决方案。

问题分析

首先,观察代码,会发现以下一些潜在问题:

  1. ARView 初始化方式 : ARViewARModel 中被初始化,使用了 UIScreen.main.bounds 来定义视图大小,然后截取了1/4的高度。这样的初始化操作看似在限定 ARView 的尺寸,实际上仅控制了 ARView 内容 渲染的区域。它的 frame 并未在整个生命周期中被改变。
  2. UIViewRepresentable 的使用 : ARViewContainer 使用 UIViewRepresentable 包裹了 ARView,使 UIKit 视图能被集成到 SwiftUI 视图层级。但是, updateUIView 方法并未进行任何的视图尺寸修改。这意味通过 SwiftUI 的布局修改影响不到 ARViewframe 属性。
  3. 错误的对齐和缩放逻辑 : 在 ContentView 中,ARViewContainer 使用了 scaledToFill 和固定的高度以及对齐。scaledToFill 会尽可能的铺满设定的容器大小,会忽略 ARViewframe。这种缩放会影响到最终视图的表现。
  4. ARSession 行为 : ARSession 会尽力捕获并显示摄像头视图,这个渲染输出并不受UIView的 frame 控制,始终会以初始的frame进行渲染,无论 ARView 的 frame 怎么变化。

总结来说,问题根本原因是 ARView 内容渲染和实际的 UIKit frame 概念的混淆,以及 SwiftUI 中直接修改 UIKit 视图布局的限制。

解决方案

以下提供了多个方案,来应对不同的场景需求:

方案一:修改 ARView 的 frame,并调整 SwiftUI 布局

  • 原理 : 方案主要是在 ARView 初始化之后,获取其父视图的 Frame, 并利用此参数去重新定义 ARView 本身的 frame 大小和起始坐标,从而控制视图本身的布局大小。此方案可以有效应对不同尺寸屏幕下视图的自适应需求。

  • 步骤

    1. 修改 ARView 初始化逻辑,ARViewframe 应跟随 UIViewController 的尺寸而动态改变。
    2. ARViewModel 中获取其父视图 Frame,在 viewDidLayoutSubviews() 生命周期中调整 ARView 的 frame.
  1. SwiftUI 部分布局中只需要使用.frame()来限定高度。
  • 代码示例:
// 重新定义 MyARView
class MyARView: ARView {
   var parentViewFrame:CGRect = .zero
   override func layoutSubviews() {
        super.layoutSubviews()
    
        if parentViewFrame != self.superview?.frame {
          parentViewFrame =  self.superview?.frame ?? .zero

           let screenRect = UIScreen.main.bounds

           let newRect = CGRect(x:  screenRect.minX , y:screenRect.minY , width: parentViewFrame.width , height: parentViewFrame.height/4)
           print(newRect)
            
           self.frame = newRect
        }
    }
}

// 修改ARModel
struct ARModel {
    private(set) var arView : MyARView
    
    init() {
        arView = MyARView()

        let config = ARWorldTrackingConfiguration()
        config.planeDetection = [.horizontal]
        arView.session.run(config)
    }
}
struct ARViewContainer : UIViewRepresentable {
    var arViewModel: ARViewModel
    
    func makeUIView(context: Context) -> MyARView {
        arViewModel.startSessionDelegate()
        return arViewModel.arView
    }
    
    func updateUIView(_ uiView: MyARView, context: Context) {}
}


struct ContentView: View {
    @ObservedObject var arViewModel : ARViewModel = ARViewModel()

    @State var mapImage: UIImage?

    var body: some View {
        
        ZStack {
            if let image = arViewModel.mapImage {
                Image(uiImage: image)
                    .resizable()
                    .scaledToFit()
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
                    .border(Color.red)
                    .background(Color.cyan)
            } else {
                Color.gray
                    .ignoresSafeArea()
                ProgressView()
            }

            ARViewContainer(arViewModel: arViewModel)
                 .frame(maxWidth: .infinity)
                .frame(height: UIScreen.main.bounds.height / 4)
                 .border(Color.blue)
                .cornerRadius(15)
               .padding(25)

                 Spacer()
            
        }.navigationBarBackButtonHidden(true)
    }
}

  • 说明 : 修改后的方案通过自定义 ARView, 并覆写了 layoutSubviews,实现了动态适配,在父视图布局发生变化的时候,会同步修改自身的 frame属性。

方案二: 使用 overlay 实现对齐

  • 原理 : 方案使用 overlay 修饰符来实现内容的对齐效果,避免了修改 ARViewframe。通过对 ARView 添加一个遮盖视图,并在遮盖视图中使用 alignment: .top 就能达到 ARView 视图的 top 对齐效果。
  • 步骤 :
    1. ARViewContainer 包装在一个 ZStack 中。
    2. ZStack 中将 ARView 和 一个空白视图以 overlay 方式布局,空白视图指定对齐方向为.top
  • 代码示例

struct ContentView: View {
    @ObservedObject var arViewModel : ARViewModel = ARViewModel()

    @State var mapImage: UIImage?

    var body: some View {
        
        ZStack {
            if let image = arViewModel.mapImage {
                Image(uiImage: image)
                    .resizable()
                    .scaledToFit()
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
                    .border(Color.red)
                    .background(Color.cyan)
            } else {
                Color.gray
                    .ignoresSafeArea()
                ProgressView()
            }

            ZStack {
              ARViewContainer(arViewModel: arViewModel)
                 .frame(maxWidth: .infinity)
                    .frame(height: UIScreen.main.bounds.height) // 这里必须指定全屏高度
                   .clipped()

                    Color.clear
                   .frame(maxWidth:.infinity)
                     .frame(height:UIScreen.main.bounds.height / 4) // 定义可见部分高度
                    .overlay(alignment: .top) {
                       
                          EmptyView()

                   }

               
           }
         
              .border(Color.blue)
            .cornerRadius(15)
            .padding(25)

               Spacer()
         }.navigationBarBackButtonHidden(true)
    }
}
  • 说明 : 此方案避免了直接操作 ARViewframe, 使用了更加符合 SwiftUI 的组合视图的布局思想来达到布局目的,使用更少的代码完成对齐任务。需要留意 ARViewContainer 视图需要充满父视图高度。

安全建议

在操作 ARKit 相关功能时, 尤其是在处理图像帧数据,需要格外注意用户隐私:

  1. 请求相机权限 : 在使用相机之前,明确请求用户的授权。
  2. 数据处理 : 在本地处理 ARKit 采集的数据,如避免不必要的数据上传或分享,防止用户隐私泄露。
  3. 用户提示 : 当用户使用摄像头相关功能时,通过指示器或 UI 反馈明确告知用户当前摄像头的状态,减少用户的误解。

这些方法可以帮助开发者在面对 ARView 大小和对齐的挑战时,有更充分的选择,同时也为应用加入了一定的安全保障。

这些方法覆盖了不同层面的调整策略。应该结合具体的应用场景选择适当的解决方案。合理运用布局的各种方式,可以让 ARViewSwiftUI 中展现的更加完美。