fatbobman / blogComments

1 stars 0 forks source link

深入了解 SwiftUI 5 中 ScrollView 的新功能 | 肘子的Swift记事本 #194

Open fatbobman opened 1 year ago

fatbobman commented 1 year ago

https://www.fatbobman.com/posts/new-features-of-ScrollView-in-SwiftUI5/

在 SwiftUI 5.0 中,苹果大幅强化了 ScrollView 功能。新增了大量新颖、完善的 API。本文将对这些新功能进行介绍,希望能够让它们更多、更早的帮助到有需要的开发者。

hiETsang commented 1 year ago

想请问一下肘子老师,如果要从 iOS15 支持起,如何才能实现类似 scrollTargetBehavior 的特性呢? 例如我有一个类似 App Store 的页面,需要横向滚动,每个屏幕 5 个元素,每次滚动停止时让元素的左边和 scrollview 边缘对齐。

fatbobman commented 1 year ago

想请问一下肘子老师,如果要从 iOS15 支持起,如何才能实现类似 scrollTargetBehavior 的特性呢? 例如我有一个类似 App Store 的页面,需要横向滚动,每个屏幕 5 个元素,每次滚动停止时让元素的左边和 scrollview 边缘对齐。

想在低版本中用原生 SwiftUI 复制 scrollTargetBehavior 的特性几乎不可能。如果你的需求,仅是实现类似 App Store 的样式,可以考虑使用一些 Pager 组件,在 Github 上有不少类似的 SwiftUI 库,搜索 ( swiftui pager)。另外,也可以考虑使用 SwiftUI 中一个尚未公开的 API ( _ScrollView + _PagingViewConfig) ,通过 https://github.com/edudnyk/SolidScroll 封装库以减少代码量。

不过,上述方案大多数都不支持 Lazy 特性。

如果上述方案都无法满足需求,也可以考虑寻找合适的 UIKit 库或自行包装

PS:对于简单的跨视图单页滚动,自己通过 dragGesture 实现,也不难。

haner0834 commented 10 months ago

@hiETsang 想请问一下肘子老师,如果要从 iOS15 支持起,如何才能实现类似 scrollTargetBehavior 的特性呢? 例如我有一个类似 App Store 的页面,需要横向滚动,每个屏幕 5 个元素,每次滚动停止时让元素的左边和 scrollview 边缘对齐。

有沒有其他更好的方法我不知道,畢竟我也只是個剛接觸程式設計的國中程式小白而已

低版本的話我是用DragGesture來實現一個頁面只有幾個View出現,但應該只能處理小量視圖

//建立一個儲存滑動量的變數
//此變數在手放開之後就會清空,所以需要在手勢結束時再做更新方法
@GestureState var dragOffset = CGSize.zero
//建立一個給予子視圖預設位置的變數
//依照自己需要的數量來決定項數即可
@State private var viewPositions: [CGFloat] = [0, 390, 390]
//0是一開始要出現的子視圖, 390式螢幕寬度外(看不到)

//再來是要前面說的會清空變數值的變數
@State private var position = CGSize.zero

//然後我的專案是會需要追蹤滑動的變量所以建立一個變數儲存
@State private var scope: Int = 0

var body: some View {
...
    ZStack {
        ForEach(0..<3, id: \.self) { i in
            Text("\(i)")
                .offset(x: viewPositions[i] + position.width + dragOffset.width)
                //這裡就是:dragOffset的變量在滑動時會有,但後面結束時會被清空,所以需要在結束時把變數值丟給position,後面會用函式包裝起來
        }
    }
    .gesture(
         DragGesture(minimumDistance: 0)
              //更新方法
              .updating($dragOffset, body: { (currentPosition, state, transaction) in
                  state = currentPosition.translation
                  //其實這裡我也不是很懂為什麼,反正我是看著做出來ㄉ(·▷.)
              })
              .onEnded({ value in
                  processDragGesture(value)
              })
     )
...
}
...
func processDragGesture(_ value: GestureStateGesture<DragGesture, CGSize>.Value) {
    //判斷滑動的方向
    var isRight: Bool {
        value.translation.width > 0
    }

    //判斷是否還能滑動
    var isBlocked: Bool = false

    self.position.width = value.translation.width
    //剛才提到的,要把滑動的偏移量存到position中

    if abs(value.translation.width) > 100 {//滑動值小於100就觸發其他動畫
        //abs(_ x:):取絕對值
        let lowStandard = 0
        let highStandard = 2
        if isRight {
            if scope > lowStandard {
            //如果大於最小值才可以觸發
                isBlocked = false

                //接下來就是一些簡單的位置變換
                //不要問我為什麼是這樣變換 我也不知道:D 反正就試出來的

                scope -= 1
                viewPositions[1] = -390 + position.width
                viewPositions[2] = 0 + position.width

                withAnimation(.easeOut(duration: 0.3)) {
                    viewPositions[0] = 0
                    viewPositions[1] = 0
                    viewPositions[2] = 390
                }

                viewPositions[0] = -390
            }else {
                isBlocked = true
            }
        }else {

            if scope < highStandard {
                isBlocked = false

                scope += 1

                viewPositions[0] = 0 + position.width
                viewPositions[1] = 390 + position.width
                viewPositions[2]+ position.width

                withAnimation(.easeOut(duration: 0.3)) {
                    viewPositions[0] = -390
                    viewPositions[1] = 0
                }

                viewPositions[2] = 390
            }else {
                isBlocked = true
            }
        }
    }

    if abs(value.translation.width) < 100 || isBlocked {
        withAnimation(.interpolatingSpring(stiffness: 50, damping: 7, initialVelocity: 5)) {
            self.position.width = 0
        }
    }else {
        self.position.width = 0
    }
}
...

說實在的這個算法很爛,但我也想不到什麼方法可以改目前的算法,畢竟這也是我第一個專案,雖然還在維護,但我還要重寫就覺得很麻煩,app叫Gremo,不介意的話可以幫我上AppStore上支持一下:D

Saafo commented 1 month ago

第二处 scrollTargetLayout( 的位置写错了,应该修饰 LazyVStack 而不是 ScrollView

fatbobman commented 1 month ago

@Saafo 今晚会改过来,谢谢