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

3,038 阅读3分钟

尝试用AI辅助我写一个类抖音的APP(一)中想让AI帮我写一个完整的APP,不现实。那么接下来,还是一步一步来吧。一口吃不成一个胖子,这点在AI里也是成立的。

探索核心部分,上下滑动翻页

最核心的功能就是滑动翻页了,于是,继续探索。上一篇尝试用AI辅助我写一个类抖音的APP(一)中Claude的回答相对靠谱点,接着问Claude:

上面的代码漏了这个功能 1.向上滑动翻页,每页一个皮肤

Claude答:

您提醒得对,我漏掉了翻页功能可以通过以下方式实现:

使用@State变量记录当前显示的皮肤索引currentPageTabView包住Skin3DView,在TabView里遍历skins数组展示每个皮肤TabView添加.tabViewStyle(.page(indexDisplayMode: .never))使其支持滑动翻页
在TabView上添加.gesture(DragGesture().onChanged{当前页码 += delta.translation.height / UIScreen.main.bounds.height})来通过拖拽手势更新currentPage
修改后的代码如下:


struct SkinView: View {
  @State private var skins = [Skin]()
  
  @State var currentPage = 0

  var body: some View {
    TabView(selection: $currentPage) {
      ForEach(skins, id: \.self) { skin in
        Skin3DView(skin: skin)
          .rotationGesture(for: .horizontal)
          .disabled(for: .vertical)
      }
    }
    .tabViewStyle(.page(indexDisplayMode: .never))
    .gesture(
      DragGesture()
        .onChanged { 
          self.currentPage -= Int($0.translation.height / UIScreen.main.bounds.height)
        }  
    )
    
    // 其他代码不变
  }
}
这样就可以通过上下拖拽TabView来实现翻页了请检查是否符合您的要求,需要我解释或修改的地方请指出,谢谢!

问ChatGPT,给出的翻页实现同样是TabView方法。ChatGPT的上下文见尝试用AI辅助我写一个类抖音的APP(一) 问:

这部分实现滑动翻页,不能做到每次翻一整页,请修改完善代码。‘’‘struct ContentView: View { @StateObject var skinViewModel = SkinViewModel() var body: some View { VStack { ScrollView { ForEach(skinViewModel.skins, id: .self) { skin in SkinView(skin: skin) } } AdView() } .onAppear { skinViewModel.fetchSkins() } } }’‘’

答:

对于实现滑动翻页(每次滑动显示一个新的页面),我们可以使用 TabView 代替 ScrollView每个 SkinView 可以作为一个独立的页面TabView 的 tabViewStyle 参数中,使用 PageTabViewStyle 来实现页面翻页的效果

以下是修改后的 ContentView:

struct ContentView: View {
    @StateObject var skinViewModel = SkinViewModel()
    var body: some View {
        VStack {
            TabView {
                ForEach(skinViewModel.skins, id: \.self) { skin in
                    SkinView(skin: skin)
                }
            }
            .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
            AdView()
        }
        .onAppear {
            skinViewModel.fetchSkins()
        }
    }
}
这样,每次滑动都会显示一个新的 SkinView 页面,实现了翻页效果

注意,PageTabViewStyle 默认会显示一个页面指示器(小点),如果你不想显示它,可以设置 indexDisplayMode 为 .never

请注意,上述代码仅为示例,可能需要根据实际项目需求进行调整

上面的两个答案,都要用TabView,但TabView只支持水平滑动,如果要垂直滑动,那么就要将页面旋转90度。有没有不用TabView实现的方案呢?问了ChatGPT,它给出了一个方案。 问:

swiftUI实现类似抖音的上滑翻页,每次滑动一整页,不用TabView实现,用Scrollview实现可以吗?给出完整代码

ChatGPT答:

SwiftUI中,可以使用ScrollView和手势识别器实现类似抖音的上滑翻页效果,不过需要注意的是,ScrollView并不直接支持分页效果,需要自己去做一些调整 以下是一个简单的例子:

import SwiftUI

struct ContentView: View {
    @State private var currentPage = 0
    let totalPage = 5

    var body: some View {
        GeometryReader { geometry in
            ScrollView(.vertical, showsIndicators: false) {
                VStack(spacing: 0) {
                    ForEach(0..<self.totalPage) { i in
                        PageView(pageNumber: i)
                            .frame(width: geometry.size.width, height: geometry.size.height)
                    }
                }
            }
            .content.offset(y: -CGFloat(self.currentPage) * geometry.size.height)
            .frame(width: geometry.size.width, height: geometry.size.height, alignment: .leading)
            .gesture(
                DragGesture()
                    .onEnded { value in
                        let offset = value.translation.height / geometry.size.height
                        let newIndex = (CGFloat(self.currentPage) - offset).rounded()
                        self.currentPage = min(max(Int(newIndex), 0), self.totalPage - 1)
                    }
            )
        }
    }
}

struct PageView: View {
    var pageNumber: Int

    var body: some View {
        VStack {
            Text("Page \(pageNumber)")
            Spacer()
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color.random)
        .edgesIgnoringSafeArea(.all)
    }
}

// 为了让每个页面的颜色不同,我们可以通过扩展Color来生成随机颜色
extension Color {
    static var random: Color {
        return Color(red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1))
    }
}
在这个例子中,一个ScrollView包含了多个PageView,每个PageView占据了整个ScrollView的宽度和高度当用户滑动ScrollView时,我们会根据滑动的距离来计算当前显示的页面,然后修改ScrollView的offset来显示正确的页面

注意:这个例子在iOS 1314上运行良好,但在15上可能会有一些问题,因为在15中,ScrollView的行为有所改变

选一个,用TabView来实现看看有什么问题

结合上面的代码,快速写了一个上滑翻页的代码

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            //写一个抖音一样的滑动视图
            TabView {
                ForEach(0..<10) { _ in
                    // Image("tikmc")
                    Image(systemName: "globe")
                        .resizable()
                        .scaledToFill()
                        .frame(width: 100, height: 100)
                        .clipped()
                        .rotationEffect(.degrees(-90))
                }
            }.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
                .rotationEffect(.degrees(90))

        }
        .padding()
    }
}

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

效果如下

tabview.gif 可以看到, 这个滑动翻页是可以实现的,但有个问题是,滑动快的时候出现一下滑动好几页的情况。活动太快出现多页的情况试了下在真机上测试,倒是不太明显。

问题2: TabView旋转90度后,显示的区域非常小,TabView的高度被限制手机屏幕宽度,所以看起来是个正方形。见下图 TabView转置显示区域受限.png

刚好昨天申请下来了Github Copilot Chat,试了一下它给的代码,问题更多。

            TabView(selection: $currentPage) {
                ForEach(0..<100) { index in
                    Skin3DView()
                        .frame(width: 1000, height: 1000)
                        .rotationEffect(.degrees(-90))
                        .gesture(DragGesture(minimumDistance: 0, coordinateSpace: .global)
                                    .onChanged({ value in
                                        if value.translation.width > 0 {
                                            if currentPage > 0 {
                                                currentPage -= 1
                                            }
                                        } else {
                                            // if currentPage < skins.count - 1 {
                                                currentPage += 1
                                            // }
                                        }
                                    }))
                }
            }
            .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
            .rotationEffect(.degrees(90))
            .frame(height: 1000)

这段代码滑动用滑动距离>0来翻页,页面切得飞快。不禁感叹,Github Copilot智能程度有限唉。

那么尝试用Scrollview直接实现上下翻页吧

这次用让AI给出答案都不行

用ScrollView实现一个模拟抖音的上下滑动翻页的swiftUI代码,要求: (1)不能用TabView,效果不好 (2)必须要用ScrollView(3)每次只能滑动一页

ChatGPT回答

SwiftUI中,我们可以使用ScrollView来实现类似抖音的上下翻页效果不过,我们需要一些额外的处理来确保每次只滑动一页,并且ScrollView没有内建的页面控制机制,所以我们需要手动处理滑动事件

下面是使用ScrollView来实现的一个例子:

import SwiftUI

struct ContentView: View {
    @State private var currentPage: Int = 0

    var body: some View {
        GeometryReader { geometry in
            ScrollView(.vertical, showsIndicators: false) {
                VStack(spacing: 0) {
                    ForEach(0..<10) { i in
                        Image("image\(i)")
                            .resizable()
                            .scaledToFit()
                            .frame(width: geometry.size.width, height: geometry.size.height)
                            .overlay(Color.black.opacity(0.4))
                    }
                }
            }
            .content.offset(y: -geometry.size.height * CGFloat(currentPage))
            .frame(width: geometry.size.width, height: nil, alignment: .top)
            .gesture(
                DragGesture().onEnded { value in
                    if value.predictedEndTranslation.height > 50, currentPage > 0 {
                        currentPage -= 1
                    } else if value.predictedEndTranslation.height < -50, currentPage < 9 {
                        currentPage += 1
                    }
                }
            )
        }
    }
}
这个例子中,我们使用了一个ScrollView并且把它的content.offset设为了基于当前页面的偏移量然后我们添加了一个DragGesture,当用户结束拖动时,我们会检查predictedEndTranslation来决定是否翻页

这个例子的一个限制是所有的图片都必须在一开始就加载,如果有大量的图片,可能会消耗大量内存对于这种情况,可能需要使用其他的方法来实现,比如自定义的ScrollView

另外,这个例子只是一个基本的实现,可能还需要处理其他的细节,比如滑动动画页面指示器等

Claude的答案也不行,这个只能自己修理了。

体验了一下抖音,抖音加载视频的时候,是从第一页开始的,在第一页下拉,会触发它的刷新。所以推测它每次是加载一个视频列表,每个视频是一个分页。 这时候可以用LazyVStack来显示每个页面,页面的高度为除了屏幕高度减去下面的tab栏高度。

一开始我自己整了一个基础的代码,是这样

struct ContentView: View {
    @State private var currentPage: Int = 0

    var body: some View {
        GeometryReader { geometry in
            ScrollView(.vertical, showsIndicators: false) {
                LazyVStack(spacing: 0) {
                    ForEach(0..<10) { i in
                        Skin3DView()
                            .frame(width: geometry.size.width, height: geometry.size.height)
                    }
                }
            }

        }.edgesIgnoringSafeArea(.all)//这行代码是问了Gpt后它加上去的
    }
}

补充问ChatGPT的问题:

修改下面的代码,实现Skin3DView铺满整个屏幕,忽略屏幕安全区域。
struct ContentView: View {
    @State private var currentPage: Int = 0


    var body: some View {
        GeometryReader { geometry in
            ScrollView(.vertical, showsIndicators: false) {
                LazyVStack(spacing: 0) {
                    ForEach(0..<10) { i in
                        Skin3DView()
                    }
                }
            }
        }
    }
}

好了,这时候可以实现页面占满屏幕,并可以滑动了。但不是抖音的那样每次滑动一页后就停止,这个是可以滑很多页。

scrollview.gif

进一步改进,让它每次只能滑动一页

在做这一步让ChatGPT给出类似抖音的滑动翻页的时候,它给出的答案都不可行。想起提问时设定ChatGPT的身份很重要,于是先对ChatGPT吹捧一番,说它是一名可靠的抖音开发,这样就相对限定了它尽可能利用抖音开发人员发的语料。这一招果然好使,下面给出的这个答案靠谱多了

你是一名可靠的抖音开发,先理解抖音的滑动翻页交互,再给上面面这段代码增加如下功能:(1)滑动距离如果超过屏幕高度的20%就滑动完一整页 (2)滑动完一整页就停止滑动
这个需求可以通过使用SwiftUI的ScrollView和DragGesture配合使用来实现以下是一个简单的实现:

struct ContentView: View {
    @State private var currentPage = 0
    @GestureState private var translation: CGFloat = 0

    var body: some View {
        GeometryReader { geometry in
            let screenHeight = geometry.size.height
            let threshold = screenHeight * 0.2
            
            ScrollView(.vertical, showsIndicators: false) {
                LazyVStack(spacing: 0) {
                    ForEach(0..<10) { i in
                        Skin3DView()
                            .frame(height: screenHeight)
                            .rotationEffect(.degrees(Double(self.translation / 5))) // Add rotation effect while dragging
                            .offset(y: self.translation)
                            .animation(.interactiveSpring(), value: self.translation)
                    }
                }.frame(height: screenHeight)
            }
            .content.offset(y: -CGFloat(self.currentPage) * screenHeight)
            .offset(y: self.translation)
            .animation(.interactiveSpring(), value: self.translation)
            .gesture(
                DragGesture()
                    .updating(self.$translation) { value, state, _ in
                        state = value.translation.height
                    }
                    .onEnded { value in
                        let offset = value.translation.height
                        if abs(offset) > threshold {
                            self.currentPage += (offset < 0 ? 1 : -1)
                        }
                    }
            )
            .edgesIgnoringSafeArea(.all)
        }
    }
}
这里我们使用了DragGesture手势,通过更新translation状态,实现了滑动时的交互动画在手势结束时,我们根据滑动的距离判断是否翻页,并更新currentPage通过对ScrollView的content.offset进行调整,实现了滑动翻页的效果

这是一个简单的实现,可能需要根据实际的需求进行调整比如,你可能需要处理滑动方向,或者添加更复杂的交互动画等

上面的答案美中不足的就是运行时页面没有停在边界上,两页之间的分界在屏幕大概中间位置。还有个问题就是没有对第一页和最后一页做越界处理。

上面的答案相对靠谱,那么就基于这个上面修改一下,应该可以实现。于是,对代码中有些不太清楚的细节,问了ChatGPT。目的是想了解代码在处理拖拽过程中做了什么。

解释一下这个代码 DragGesture() .updating(self.$translation) { value, state, _ in state = value.translation.height }
这段代码是SwiftUI中的拖拽手势(DragGesture)的使用

首先,DragGesture()创建了一个拖拽手势

.updating(self.$translation) { value, state, _ in state = value.translation.height }这行代码做了以下几件事:

这里用到了SwiftUI的@GestureState属性包装器,它可以存储手势的临时状态在这个例子中,translation就是存储手势的临时状态

.updating(:_:_:)方法会在手势发生改变时被调用,它有三个参数:

第一个参数是一个绑定(Binding),在这个例子中是self.$translation它指向了保存手势状态的变量
第二个参数是一个闭包,会在手势发生改变时被调用闭包中有三个参数:
value:表示手势的当前值在这个例子中,value.translation.height表示手势在垂直方向上的偏移量
state:表示手势的临时状态在闭包内部,我们将手势的垂直偏移量赋值给了state,这样translation就被更新了
最后一个参数是一个Transaction,它包含了手势改变的详细信息,比如时间和动画在这个例子中,我们不需要这个参数,所以用_忽略了它
总的来说,这段代码的作用是:在拖拽手势发生变化时,将手势在垂直方向上的偏移量存储到translation中

ChatGPT这个解释很准确了,就是@GestureState private var translation: CGFloat = 0这个变量是用来获取滑动的位移的,updating中的state变量要用参数value来更新位移,这样才能传给translation。
有了实时的位移信息,便可以通过改变content offset来改变scrollview的位移,见代码 .content.offset(y: -CGFloat(self.currentPage) * screenHeight),其中translation影响了currentPage从而影响了content offset。

因为currentPage不是一个连续的值,那么滑动前后会导致content offset的跳变,要想实时连续地看到滑动的效果,代码中增加了 .offset(y: self.translation)这一行。translation在滑动时有偏移,手指松开滑动结束onEnded处理完后便为0了,所以不影响页面的偏移。在onEnded里更新了currentPage,content offset也有了正确的值。
理解了代码思路后,我们就可以针对性地修改了。核心是 content offset的改变, 外加过程动画。过程动画整个scrollview有一个offset就可以了, 把Skin3DView里的offset和动画删除。通过调试发现.edgesIgnoringSafeArea(.all)要放在GeometryReader这一层才能忽略屏幕安全区域,其实它的上面一次回答的代码是正确的,这次却放错地方了。另外就是LazyVStack不用指定高度,删除它的.frame(height: screenHeight)。 再次运行,非常完美。

scrollview核心翻页功能完成1.gif

附上代码

struct ContentView: View {
    @State private var currentPage = 0
    @GestureState private var translation: CGFloat = 0

    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) {
                LazyVStack(spacing: 0) {
                    ForEach(0..<10) { i in
                        Skin3DView()
                            .frame(height: screenHeight)
//                            .rotationEffect(.degrees(Double(self.translation / 5))) // Add rotation effect while dragging
//                            .offset(y: self.translation)
//                            .animation(.interactiveSpring(), value: self.translation)
                    }
                }//.frame(height: screenHeight)
            }
            .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
                        state = value.translation.height
                    }
                    .onEnded { value in
                        let offset = value.translation.height
                        if abs(offset) > threshold {
                            if offset < 0 && self.currentPage < 9 {//上滑,翻下一页
                                self.currentPage += 1
                            }else if offset > 0 && self.currentPage > 0{
                                self.currentPage -= 1
                            }
                        }
                    }
            )
            
        }.edgesIgnoringSafeArea(.all)
    }
}

总结

ChatGPT4在回答问题的准确程度上,的确是比Claude要好。内容的新旧程度上也比Claude要新,例如Claude的答案没有用SwiftUI的SceneView,还是用包装的UIKit中的SceneKit,但ChatGPT4有。 另外,要用好AI,给它一个人设很重要,我理解是可以大概圈定它的语料范围,不会满世界给你找一个似乎相关的答案。