Pociągnij w dół, aby odświeżyć dane w SwiftUI

Użyłem prostej listy danych za pomocą List. Chciałbym dodać pull down, aby odświeżyć funkcjonalność, ale nie jestem pewien, które jest najlepsze możliwe podejście.

Pociągnij w dół, aby odświeżyć widok będzie widoczny tylko wtedy, gdy użytkownik spróbuje pociągnąć w dół z pierwszego indeksu, tak jak to zrobiliśmy w UITableView z UIRefreshControl w UIKit

Oto prosty kod do wyświetlania danych w SwiftUI.

struct CategoryHome: View {
    var categories: [String: [Landmark]] {
        .init(
            grouping: landmarkData,
            by: { $0.category.rawValue }
        )
    }

    var body: some View {
        NavigationView {
            List {
                ForEach(categories.keys.sorted().identified(by: \.self)) { key in
                    Text(key)
                }
            }
            .navigationBarTitle(Text("Featured"))
        }
    }
}
Author: PinkeshGjr, 2019-06-07

6 answers

Potrzebowałem tego samego do aplikacji, z którą się bawię, i wygląda na to, że SwiftUI API nie zawiera możliwości odświeżania dla ScrollView s w tej chwili.

Z czasem API będzie rozwijało i naprawiało tego typu sytuacje, ale ogólnym rozwiązaniem problemu braku funkcjonalności w SwiftUI zawsze będzie implementacja struktury implementującej UIViewRepresentable. Oto szybki i brudny dla UIScrollView z kontrolką odświeżania.

struct LegacyScrollView : UIViewRepresentable {
    // any data state, if needed

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    func makeUIView(context: Context) -> UIScrollView {
        let control = UIScrollView()
        control.refreshControl = UIRefreshControl()
        control.refreshControl?.addTarget(context.coordinator, action:
            #selector(Coordinator.handleRefreshControl),
                                          for: .valueChanged)

        // Simply to give some content to see in the app
        let label = UILabel(frame: CGRect(x: 0, y: 0, width: 200, height: 30))
        label.text = "Scroll View Content"
        control.addSubview(label)

        return control
    }


    func updateUIView(_ uiView: UIScrollView, context: Context) {
        // code to update scroll view from view state, if needed
    }

    class Coordinator: NSObject {
        var control: LegacyScrollView

        init(_ control: LegacyScrollView) {
            self.control = control
        }

        @objc func handleRefreshControl(sender: UIRefreshControl) {
            // handle the refresh event

            sender.endRefreshing()
        }
    }
}

Ale oczywiście, nie możesz użyć żadnego SwiftUI komponenty w widoku przewijania bez zawijania ich w UIHostingController i upuszczania w makeUIView, zamiast umieszczania ich w LegacyScrollView() { // views here }.

 23
Author: Evan Deaubl,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-06-11 14:21:31

Oto implementacja, która introspektuje hierarchię widoku i dodaje odpowiednie UIRefreshControl do widoku tabeli listy SwiftUI: https://github.com/timbersoftware/SwiftUIRefresh

Większość logiki introspekcji można znaleźć tutaj: https://github.com/timbersoftware/SwiftUIRefresh/blob/15d9deed3fec66e2c0f6fd1fd4fe966142a891db/Sources/PullToRefresh.swift#L39-L73

 15
Author: ldiqual,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-11-26 02:04:18

Oto proste, małe i czyste rozwiązanie SwiftUI zrobiłem w celu dodania funkcji pull do odświeżania do widoku przewijania.

struct PullToRefresh: View {
    
    var coordinateSpaceName: String
    var onRefresh: ()->Void
    
    @State var needRefresh: Bool = false
    
    var body: some View {
        GeometryReader { geo in
            if (geo.frame(in: .named(coordinateSpaceName)).midY > 50) {
                Spacer()
                    .onAppear {
                        needRefresh = true
                    }
            } else if (geo.frame(in: .named(coordinateSpaceName)).maxY < 10) {
                Spacer()
                    .onAppear {
                        if needRefresh {
                            needRefresh = false
                            onRefresh()
                        }
                    }
            }
            HStack {
                Spacer()
                if needRefresh {
                    ProgressView()
                } else {
                    Text("⬇️")
                }
                Spacer()
            }
        }.padding(.top, -50)
    }
}

Aby go użyć, wystarczy dodać go u góry widoku przewijania i nadać mu przestrzeń współrzędnych widoku przewijania:

ScrollView {
    PullToRefresh(coordinateSpaceName: "pullToRefresh") {
        // do your stuff when pulled
    }
    
    Text("Some view...")
}.coordinateSpace(name: "pullToRefresh")
 7
Author: Anthony,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-12-02 01:25:15

Cześć, sprawdź tę bibliotekę, którą zrobiłem: https://github.com/AppPear/SwiftUI-PullToRefresh

Możesz zaimplementować go za pomocą jednej linii kodu:

struct CategoryHome: View {
    var categories: [String: [Landmark]] {
        .init(
            grouping: landmarkData,
            by: { $0.category.rawValue }
        )
    }

    var body: some View {
        RefreshableNavigationView(title: "Featured", action:{
           // your refresh action
        }){
                ForEach(categories.keys.sorted().identified(by: \.self)) { key in
                    Text(key)
                    Divider() // !!! this is important to add cell separation
                }
            }
        }
    }
}
 5
Author: Samu Andras,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-09-19 15:27:53

Swiftui-introspects nie jest jeszcze obsługiwany na masOS, więc jeśli zamierzasz zbudować interfejs, który działa zarówno na iOS, jak i macOS, rozważ bibliotekę Samu Andras.

Rozwidlałem jego kod, dodałem kilka ulepszeń i dodałem możliwość korzystania bez widoku nawigacyjnego

Oto przykładowy kod.

RefreshableList(showRefreshView: $showRefreshView, action:{
                           // Your refresh action
                            // Remember to set the showRefreshView to false
                            self.showRefreshView = false

                        }){
                            ForEach(self.numbers, id: \.self){ number in
                                VStack(alignment: .leading){
                                    Text("\(number)")
                                    Divider()
                                }
                            }
                        }

Aby uzyskać więcej informacji, odwiedź poniższy link. https://github.com/phuhuynh2411/SwiftUI-PullToRefresh

 1
Author: Phu HUYNH,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-04-06 04:31:57

Próbowałem wielu różnych rozwiązań, ale nic nie działało wystarczająco dobrze dla mojego przypadku. GeometryReader rozwiązania oparte miały złą wydajność dla złożonego układu.

[11]}Oto czysty widok SwiftUI 2.0, który wydaje się działać dobrze, nie zmniejsza wydajności przewijania przy ciągłych aktualizacjach stanu i nie używa żadnych hacków UIKit: {]}
import SwiftUI

struct PullToRefreshView: View
{
    private static let minRefreshTimeInterval = TimeInterval(0.2)
    private static let triggerHeight = CGFloat(100)
    private static let indicatorHeight = CGFloat(100)
    private static let fullHeight = triggerHeight + indicatorHeight
    
    let backgroundColor: Color
    let foregroundColor: Color
    let isEnabled: Bool
    let onRefresh: () -> Void
    
    @State private var isRefreshIndicatorVisible = false
    @State private var refreshStartTime: Date? = nil
    
    init(bg: Color = .white, fg: Color = .black, isEnabled: Bool = true, onRefresh: @escaping () -> Void)
    {
        self.backgroundColor = bg
        self.foregroundColor = fg
        self.isEnabled = isEnabled
        self.onRefresh = onRefresh
    }
    
    var body: some View
    {
        VStack(spacing: 0)
        {
            LazyVStack(spacing: 0)
            {
                Color.clear
                    .frame(height: Self.triggerHeight)
                    .onAppear
                    {
                        if isEnabled
                        {
                            withAnimation
                            {
                                isRefreshIndicatorVisible = true
                            }
                            refreshStartTime = Date()
                        }
                    }
                    .onDisappear
                    {
                        if isEnabled, isRefreshIndicatorVisible, let diff = refreshStartTime?.distance(to: Date()), diff > Self.minRefreshTimeInterval
                        {
                            onRefresh()
                        }
                        withAnimation
                        {
                            isRefreshIndicatorVisible = false
                        }
                        refreshStartTime = nil
                    }
            }
            .frame(height: Self.triggerHeight)
            
            indicator
                .frame(height: Self.indicatorHeight)
        }
        .background(backgroundColor)
        .ignoresSafeArea(edges: .all)
        .frame(height: Self.fullHeight)
        .padding(.top, -Self.fullHeight)
    }
    
    private var indicator: some View
    {
        ProgressView()
            .progressViewStyle(CircularProgressViewStyle(tint: foregroundColor))
            .opacity(isRefreshIndicatorVisible ? 1 : 0)
    }
}
Po wejściu do ekranu lub wyjściu z niego, wyświetlane są znaki onAppear i onDisappear w widoku wyzwalacza Color.clear.

Odświeżanie jest wyzwalane, jeśli czas pomiędzy pojawieniem się i zniknięciem widoku wyzwalacza jest większy niż minRefreshTimeInterval, aby umożliwić odbicie ScrollView bez wyzwalania odświeżania.

Aby go użyć Dodaj PullToRefreshView do góry ScrollView:

import SwiftUI

struct RefreshableScrollableContent: View
{
    var body: some View
    {
        ScrollView
        {
            VStack(spacing: 0)
            {
                PullToRefreshView { print("refreshing") }
                
                // ScrollView content
            }
        }
    }
}

Gist: https://gist.github.com/tkashkin/e5f6b65b255b25269d718350c024f550

 1
Author: Anatoliy Kashkin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2021-02-09 17:41:32