尝试用AI辅助我写一个类抖音的APP(四)

2,085 阅读7分钟

重新捡起这个项目。

系列文章

尝试用AI辅助我写一个类抖音的APP(一)中探索了AI不能一下把APP的所有功能实现出来。
尝试用AI辅助我写一个类抖音的APP(二)中探索和实现了核心的滑动翻页部分。
尝试用AI辅助我写一个类抖音的APP(三) 记录了在用LazyVStack下大量View会全部调用它们的init和对body求值的问题

这一期记录一个实现翻页情况下,滑动的过程是一个颜色,滑动完后页面又重新布局了,换了另外一个颜色。

output.gif

原始代码

Content页面

import SwiftUI


var isLanguageChinese = false


struct ContentView: View {
    @State private var currentPage = 0
    @GestureState private var translation: CGFloat = 0
    @StateObject var skins : SuitListModel = SuitListModel(sex: .boy)
    
    @State var frozen: Bool = false
    @State var refreshState: MtRefreshState = .initial

    var body: some View {
        GeometryReader { geometry in
            let screenHeight = geometry.size.height
            let threshold = screenHeight * 0.1  //这里把0.2改成0.1更顺畅了
            
            ScrollView(.vertical, showsIndicators: false) {
//                ZStack(alignment: .top ){
//                    RefreshHeader(height: 70,
//                                  loading: true,
//                                  frozen: true,
//                                  rotation: Angle(degrees: 0),
//                                  updateTime: Date.now,
//                                  freshState: .pulling)
                    LazyVStack(spacing: 0) {
                        ForEach(0..<100){ i in
                            //如果index和currentPage相差大于2,就不显示showSceneView
                            Skin3DView(index: i, currentPage: $currentPage)
                                .frame(height: screenHeight)
                        }
                        //                    ForEach(skins.list, id: \.uuid) { s in
                        //                        SuitDetailView(suit: s)//.environment(\.colorScheme, .dark)
                        //                            .environmentObject(ImageModel(img: s.img, armpx: s.armpx))
                        //                            .frame(height: screenHeight)
                        //                    }
//                    }
//                    RefreshHeader(height: 70,
//                                  loading: true,
//                                  frozen: true,
//                                  rotation: Angle(degrees: 0),
//                                  updateTime: Date.now,
//                                  freshState: .pulling)
//                    .alignmentGuide(.top, computeValue: { viewDimensions in
//
//                        return viewDimensions[.bottom] - 140
//                    })
                }
            }
            .content.offset(y: -CGFloat(self.currentPage) * screenHeight)//定位到整页位置,实现翻页
            .offset(y: self.translation)    //在没有翻页前的跟随手指的滑动效果
//            .animation(.interactiveSpring(), value: self.translation)
            .animation(.interactiveSpring(), value: self.currentPage)
            .gesture(
                DragGesture()
                    .updating(self.$translation) { value, state, _ in
                        //判断第一页下拉刷新,
                        //如果进入刷新状态, 且下拉超过 70的距离, 那么进入固定frozen状态
                        //如果这是松开手,那么开启刷新,triger状态
                        //刷新完毕,回到初始状态
                        //判断是否第一页下拉(触发刷新)
//                        refreshState = stateMachineRefresh(currentState: refreshState, gesture: "pull", isFirstPage: currentPage == 0, translation: value.translation.height)
//                        if refreshState == .full{
//                            state = 70
//                        }else{
//                            state = value.translation.height
//                        }
                            state = value.translation.height
                    }
                    .onEnded { value in
                        let offset = value.translation.height
                        //状态机判断
                        refreshState = stateMachineRefresh(currentState: refreshState, gesture: "end", isFirstPage: currentPage == 0, translation: offset)
                        
                        //如果滑动超过
                        if abs(offset) > threshold {
                            if offset < 0 && self.currentPage < 999 {//上滑,翻下一页
                                self.currentPage += 1
                            }else if offset > 0 && self.currentPage > 0{
                                self.currentPage -= 1
                            }
                        }
                    }
            )
            //接收到json皮肤更新
//            .onReceive(NotificationCenter.default.publisher(for: Notification.Name("jsonupdated"))){
//                data in
//                mtlog.info("receive jsonupdated")
////                skins.reload()
//            }
            
        }.edgesIgnoringSafeArea(.all)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Skin3DView

import SwiftUI
import SceneKit

struct Skin3DView: View {
    var index: Int//属于第几个索引页
    @Binding var currentPage: Int    //受限于同时只能显示32个SceneView,设置这个变量来控制同时显示的SceneView数量
    init(index: Int, currentPage: Binding<Int>){
        self.index = index
//        mtlog.info("Skin3DView init() \(index)")
        //初始化showSceneView
        self._currentPage = currentPage
    }
    var body: some View {
//        ZStack{
//            let _ = mtlog.info("Skin3DView body \(index)")
//            Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
            //添加背景,颜色随机
            Rectangle()
                .foregroundColor(Color.randomColor())
                .edgesIgnoringSafeArea(.all)
//            if abs(currentPage - index) < 2{
//                SceneView(scene: SCNScene(named: "3dmodel.scnassets/alex20220426.dae"), options: .autoenablesDefaultLighting)
//            }
//        }
    }
}
extension Color {
    static func randomColor() -> Color {
        return Color(red: Double.random(in: 0...1),
                     green: Double.random(in: 0...1),
                     blue: Double.random(in: 0...1))
    }
    
}

struct Skin3DView_Previews: PreviewProvider {
    static var previews: some View {
        Skin3DView(index: 1, currentPage: .constant(1))
    }
}

寻找原因

为什么滑动完一整页后,页面会被重新渲染呢
问ChatGPT

image.png

再问

分页滚动过程中和滚动完成后,为什么页面的颜色变了

ChatGPT回答

页面的颜色在分页滚动过程中和滚动完成后发生变化,通常是由以下原因之一导致的:

1. **页面背景色的条件渲染**:代码中可能存在基于当前页面状态来改变背景颜色的逻辑。例如,在不同页面或者根据页面滚动的方向、位置来设置不同的背景颜色。
  
2. **视图的初始化或状态重置**:在视图重新绘制时(例如滚动到新的页面),可能会重新初始化或者更新某些属性,导致颜色改变。比如,使用 `@State`、`@Binding` 或 `@Environment` 时,视图的属性可能根据状态变化自动更新。

3. **动画或过渡效果**:某些 SwiftUI 组件(比如 `ScrollView`)在滚动时可能带有动画或过渡效果,可能通过过渡效果将颜色从一种状态变为另一种。

4. **3D渲染的影响**:如果你在3D视图中使用了特殊的渲染效果、着色器或光照,它们可能会在视角或场景改变时引起颜色的变化。

你可以检查 `Skin3DView` 和 `ContentView` 中的代码,看看是否存在上述任何逻辑。

它给出了通用的回答,没有结合我的代码来说,所以我感觉没有回答到点子上。 继续问

swiftui中为什么body这个也会不停的执行呢

ChatGPT回答

在 SwiftUI 中,视图的 `body` 属性会频繁重新计算,这是 SwiftUI 响应式更新机制的结果。这个机制基于 SwiftUI 声明式编程范式和状态驱动的设计。

`body` 属性频繁执行的原因:
1. **状态改变**:当使用 `@State`、`@Binding`、`@ObservedObject` 或 `@EnvironmentObject` 等修饰符的属性发生改变时,SwiftUI 会重新计算依赖这些属性的视图 `body`,从而触发视图更新。

2. **父视图的重绘**:如果某个视图的父视图发生了重绘,那么 SwiftUI 会重新计算子视图的 `body`。这种重绘可能由父视图的状态改变或者外部环境的变化导致。

3. **动画**:动画会引起视图的不断重绘,因为 SwiftUI 需要根据动画的进度来逐帧更新视图。

如何优化:
虽然 `body` 频繁执行是 SwiftUI 的设计初衷,但可以通过以下方式进行优化:
1. **避免不必要的状态更新**:尽量将状态和逻辑封装在特定视图中,减少全局状态的使用。

2. **优化子视图**:将 `body` 方法复杂度高的视图拆分成更小的组件,并将状态最小化。

3. **`@ViewBuilder` 和 `@View` 优化**:通过使用 `@ViewBuilder` 和 `@View` 减少子视图的重绘。

总之,虽然 SwiftUI 中 `body` 方法频繁执行是正常现象,但可以通过优化状态管理和视图结构来减少不必要的性能开销。

这个也是通用的回答,没有结合代码来说,可能也是我的问法不对。

直到我看了这篇文章,避免 SwiftUI 视图的重复计算

image.png

通过查看代码, 颜色背景页Skin3DView的参数里就有binding类型的currentPage这个参数。

struct Skin3DView: View {
    var index: Int//属于第几个索引页
    @Binding var currentPage: Int    //受限于同时只能显示32个SceneView,设置这个变量来控制同时显示的SceneView数量
    init(index: Int, currentPage: Binding<Int>){
        self.index = index
//        mtlog.info("Skin3DView init() \(index)")
        //初始化showSceneView
        self._currentPage = currentPage
    }

currentPage这个参数在Content里,滑动一页结束的时候,被修改了

                    .onEnded { value in
                        let offset = value.translation.height
                        //状态机判断
                        refreshState = stateMachineRefresh(currentState: refreshState, gesture: "end", isFirstPage: currentPage == 0, translation: offset)
                        
                        //如果滑动超过
                        if abs(offset) > threshold {
                            if offset < 0 && self.currentPage < 999 {//上滑,翻下一页
                                self.currentPage += 1    //****在这里被修改了
                            }else if offset > 0 && self.currentPage > 0{
                                self.currentPage -= 1    //****在这里被修改了
                            }
                        }
                    }

也就是像这种参数被修改后,会导致Skin3DView的重绘,不管实际页面是否用到currentPage,都会重新求值Skin3DView的body属性,所以颜色就变了。

解决

知道原因后,就可以针对性地找解决方案了。把Skin3DView的binding参数去掉

struct Skin3DView: View {
    var index: Int//属于第几个索引页
//    @Binding var currentPage: Int    //受限于同时只能显示32个SceneView,设置这个变量来控制同时显示的SceneView数量
    init(index: Int){
        self.index = index
//        mtlog.info("Skin3DView init() \(index)")
        //初始化showSceneView
//        self._currentPage = currentPage
    }
    var body: some View {
        ZStack{
//            let _ = mtlog.info("Skin3DView body \(index)")
//            Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
            //添加背景,颜色随机
            Rectangle()
                .foregroundColor(Color.randomColor())
                .edgesIgnoringSafeArea(.all)
//            if abs(currentPage - index) < 2{
//                SceneView(scene: SCNScene(named: "3dmodel.scnassets/alex20220426.dae"), options: .autoenablesDefaultLighting)
//            }
        }
    }
}

看看效果 可以看到,这时候滑动到完整一页。它都不会临时变色了。

output1.gif

后记

其实这个问题问ChatGPT的时候,它的回答也没有错:的确是binding类型的变量导致了View重绘,但有个细节没说到就是不论是否View是否用到这个binding变量,这个View都会重绘。只是我潜意识地怀疑它会答非所问,给不出正确答案,忽略了它的回答。当然它的回答没有结合代码来说也是很大的问题。

这个背景色会变只是开发过程中的一个疑问, 至于最终是否要引入currentPage 变量,还是要看实际情况。不引入currentPage变量,目前还没想到怎么解决尝试用AI辅助我写一个类抖音的APP(三)中的LazyVStack里大量3d View的性能问题。