SwiftUI: добавить возможность обновления в LazyVStack?

Когда я использую представление списка, я могу легко добавить refreshableмодификатор для запуска логики обновления. Мой вопрос заключается в том, как добиться того же при использовании LazyVStack.

У меня есть следующий код:

      struct TestListView: View {
    
    var body: some View {
        
        Text("the list view")
        
        // WORKS:
//        VStack {
//            List {
//                ForEach(0..<10) { n in
//                    Text("N = \(n)")
//                }
//            }
//            .refreshable {
//
//            }
//        }
        
        
        // DOES NOT SHOW REFRESH CONTROL:
        ScrollView {
            
            LazyVStack {
                ForEach(0..<10) { n in
                    Text("N = \(n)")
                }
            }

        }
        .refreshable {
            
        }
        
    }
    
}

Как я могу получить поведение pull для обновления в LazyVStackкейс?

4 ответа

На самом деле это возможно, и я бы сказал, что я понимаю идею Apple - они дают встроенное поведение по умолчанию для тяжелых List, но оставить легким ScrollViewтолько что подготовленный, чтобы мы могли настроить его так, как нам нужно.

Итак, вот демонстрация решения (протестировано с Xcode 13.4/iOS 15.5)

Основная часть:

      struct ContentView: View {
    var body: some View {
        ScrollView {
            RefreshableView()
        }
        .refreshable {     // << injects environment value !!
            await fetchSomething()
        }
    }
}


struct RefreshableView: View {
    @Environment(\.refresh) private var refresh   // << refreshable injected !!

    @State private var isRefreshing = false

    var body: some View {
        VStack {
            if isRefreshing {
                MyProgress()
                    .transition(.scale)
            }
        // ...
        .onPreferenceChange(ViewOffsetKey.self) {
            if $0 < -80 && !isRefreshing {   // << any creteria we want !!
                isRefreshing = true
                Task {
                    await refresh?()           // << call refreshable !!
                    await MainActor.run {
                        isRefreshing = false
                    }
                }
            }
        }

Полный тестовый модуль здесь

Теперь SwiftUI добавил.refreshableмодификатор кScrollView.

Просто используйте его так, как вы делаете сList

      ScrollView {
    LazyVStack {
        // Loop and add View
    }
}
.refreshable {
    refreshLogic()
}

Однако поддерживается запуск iOS 15.

Вот ссылка на документацию

      @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
extension View {
    /// Marks this view as refreshable.
    public func refreshable(action: @escaping @Sendable () async -> Void) -> some View
}

На основе ответа Аспери :

      import SwiftUI

struct ContentView: View {
    var body: some View {
        ScrollView {
            RefreshableView {
                RoundedRectangle(cornerRadius: 20)
                    .fill(.red).frame(height: 100).padding()
                    .overlay(Text("Button"))
                    .foregroundColor(.white)
            }
        }
        .refreshable {     // << injects environment value !!
            await fetchSomething()
        }
    }

    func fetchSomething() async {
        // demo, assume we update something long here
        try? await Task.sleep(nanoseconds: 2 * 1_000_000_000)
    }
}

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

struct RefreshableView<Content: View>: View {
    
    var content: () -> Content
    
    @Environment(\.refresh) private var refresh   // << refreshable injected !!
    @State private var isRefreshing = false

    var body: some View {
        VStack {
            if isRefreshing {
                MyProgress()    // ProgressView() ?? - no, it's boring :)
                    .transition(.scale)
            }
            content()
        }
        .animation(.default, value: isRefreshing)
        .background(GeometryReader {
            // detect Pull-to-refresh
            Color.clear.preference(key: ViewOffsetKey.self, value: -$0.frame(in: .global).origin.y)
        })
        .onPreferenceChange(ViewOffsetKey.self) {
            if $0 < -80 && !isRefreshing {   // << any creteria we want !!
                isRefreshing = true
                Task {
                    await refresh?()           // << call refreshable !!
                    await MainActor.run {
                        isRefreshing = false
                    }
                }
            }
        }
    }
}

struct MyProgress: View {
    @State private var isProgress = false
    var body: some View {
        HStack{
             ForEach(0...4, id: \.self){index in
                  Circle()
                        .frame(width:10,height:10)
                        .foregroundColor(.red)
                        .scaleEffect(self.isProgress ? 1:0.01)
                        .animation(self.isProgress ? Animation.linear(duration:0.6).repeatForever().delay(0.2*Double(index)) :
                             .default
                        , value: isProgress)
             }
        }
        .onAppear { isProgress = true }
        .padding()
    }
}

public struct ViewOffsetKey: PreferenceKey {
    public typealias Value = CGFloat
    public static var defaultValue = CGFloat.zero
    public static func reduce(value: inout Value, nextValue: () -> Value) {
        value += nextValue()
    }
}

Просто создайте файл в своем проекте

      public struct RefreshableScrollView<Content: View>: View {
    var content: Content
    var onRefresh: () -> Void

    public init(content: @escaping () -> Content, onRefresh: @escaping () -> Void) {
        self.content = content()
        self.onRefresh = onRefresh
    }

    public var body: some View {
        List {
            content
                .listRowSeparatorTint(.clear)
                .listRowBackground(Color.clear)
                .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
        }
        .listStyle(.plain)
        .refreshable {
            onRefresh()
        }
    }
}

затем используйтеRefreshableScrollViewв любом месте вашего проекта

пример:

       RefreshableScrollView{
      // Your content LaztVStack{}
    } onRefresh: {
      // do something you want
    }
Другие вопросы по тегам