尝试用AI辅助我写一个类抖音的APP(一)中想让AI帮我写一个完整的APP,不现实。那么接下来,还是一步一步来吧。一口吃不成一个胖子,这点在AI里也是成立的。
探索核心部分,上下滑动翻页
最核心的功能就是滑动翻页了,于是,继续探索。上一篇尝试用AI辅助我写一个类抖音的APP(一)中Claude的回答相对靠谱点,接着问Claude:
上面的代码漏了这个功能 1.向上滑动翻页,每页一个皮肤
Claude答:
您提醒得对,我漏掉了翻页功能。可以通过以下方式实现:
使用@State变量记录当前显示的皮肤索引currentPage
用TabView包住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 13和14上运行良好,但在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()
}
}
效果如下
可以看到, 这个滑动翻页是可以实现的,但有个问题是,滑动快的时候出现一下滑动好几页的情况。活动太快出现多页的情况试了下在真机上测试,倒是不太明显。
问题2:
TabView旋转90度后,显示的区域非常小,TabView的高度被限制手机屏幕宽度,所以看起来是个正方形。见下图
刚好昨天申请下来了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()
}
}
}
}
}
}
好了,这时候可以实现页面占满屏幕,并可以滑动了。但不是抖音的那样每次滑动一页后就停止,这个是可以滑很多页。
进一步改进,让它每次只能滑动一页
在做这一步让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)。
再次运行,非常完美。
附上代码
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,给它一个人设很重要,我理解是可以大概圈定它的语料范围,不会满世界给你找一个似乎相关的答案。