SwiftUI Generic Pull to refresh view
Here is possible approach (compiled with Xcode 11.4)
Usage:
CustomScrollView(width: 100, height: 100,
viewModel: HomeViewMode()) {
HomeView()
}
Generic type:
protocol CustomViewModel {
func loadPackages()
}
// It is used generic ViewBuilder pattern for content
struct CustomScrollView<Content: View, VM: CustomViewModel> : UIViewRepresentable {
var width : CGFloat
var height : CGFloat
let viewModel: VM
let content: () -> Content
init(width: CGFloat, height: CGFloat, viewModel: VM, @ViewBuilder content: @escaping () -> Content) {
self.width = width
self.height = height
self.viewModel = viewModel
self.content = content
}
func makeUIView(context: Context) -> UIScrollView {
let control = UIScrollView()
control.refreshControl = UIRefreshControl()
control.refreshControl?.addTarget(context.coordinator, action: #selector(Coordinator.handleRefreshControl), for: .valueChanged)
let childView = UIHostingController(rootView: content())
childView.view.frame = CGRect(x: 0, y: 0, width: width, height: height)
control.addSubview(childView.view)
return control
}
func updateUIView(_ uiView: UIScrollView, context: Context) { }
class Coordinator: NSObject {
var control: CustomScrollView<Content, VM>
var viewModel: VM
init(_ control: CustomScrollView, viewModel: VM) {
self.control = control
self.viewModel = viewModel
}
@objc func handleRefreshControl(sender: UIRefreshControl) {
sender.endRefreshing()
viewModel.loadPackages()
}
}
}
Pull down to refresh data in SwiftUI
I needed the same thing for an app I'm playing around with, and it looks like the SwiftUI API does not include a refresh control capability for ScrollView
s at this time.
Over time, the API will develop and rectify these sorts of situations, but the general fallback for missing functionality in SwiftUI will always be implementing a struct that implements UIViewRepresentable
. Here's a quick and dirty one for UIScrollView
with a refresh control.
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()
}
}
}
But of course, you can't use any SwiftUI components in your scroll view without wrapping them in a UIHostingController
and dropping them in makeUIView
, rather than putting them in a LegacyScrollView() { // views here }
.
SwiftUI List not updating inside pull to refresh view
It appears that dynamic property update cannot pass boundary of different hosting controller, so the solution is pass it (in this case observable object) inside explicitly.
Tested with Xcode 11.4 / iOS 13.4 on replicated code
So, custom view is constructed as
CustomScrollView(width: geometry.size.width, height: geometry.size.height, viewModel: self.seeAllViewModel) {
// Separate internals to subview and pass view modal there
RefreshInternalView(seeAllViewModel: self.seeAllViewModel)
}
and here is separated view, nothing special - just extracted everything from above
struct RefreshInternalView: View {
@ObservedObject var seeAllViewModel: SeeAllViewModel
var body: some View {
VStack {
Text("\(self.seeAllViewModel.category.items.count)") // not being updated
List {
ForEach(self.seeAllViewModel.category.items) { (item: Item) in
ItemRowView(itemViewModel: ItemViewModel(item: item))
}
}
.listStyle(GroupedListStyle())
.navigationBarTitle(Text(self.seeAllViewModel.category.title.firstCapitalized))
}
}
}
Introspect library UIRefreshControl with SwiftUI not working
The introspectTableView
closure is being called every time the view is refreshed, which is why the code to get new data keeps being executed.
To solve this, what you need to is to call your refreshing code only when the user has been pulled. The only way I could do this was by creating a RefreshHelper class, since the UIRefreshControl still depends on @objc
functions and Selectors.
Here's a sample that roughly implements what you want:
struct ContentView: View {
@State var data: [String] = (0...5).map { "Item \($0)" }
let refreshHelper = RefreshHelper()
class RefreshHelper {
var parent: ContentView?
var refreshControl: UIRefreshControl?
@objc func didRefresh() {
guard let parent = parent, let refreshControl = refreshControl else { return }
parent.data.append("Item \(parent.data.count)") // load data here
refreshControl.endRefreshing()
}
}
var body: some View {
List(data, id: \.self) { i in
Text("Item \(i)")
}
.introspectTableView { tableView in
let control = UIRefreshControl()
refreshHelper.parent = self
refreshHelper.refreshControl = control
control.addTarget(refreshHelper, action: #selector(RefreshHelper.didRefresh), for: .valueChanged)
tableView.refreshControl = control
}
}
}
How to use pull to refresh in Swift?
Pull to refresh is built in iOS. You could do this in swift like
let refreshControl = UIRefreshControl()
override func viewDidLoad() {
super.viewDidLoad()
refreshControl.attributedTitle = NSAttributedString(string: "Pull to refresh")
refreshControl.addTarget(self, action: #selector(self.refresh(_:)), for: .valueChanged)
tableView.addSubview(refreshControl) // not required when using UITableViewController
}
@objc func refresh(_ sender: AnyObject) {
// Code to refresh table view
}
At some point you could end refreshing.
refreshControl.endRefreshing()
Initialising a SwiftUI View via a Swift generic
You can request it to be injected externally, like
struct GenericRefreshView<T, V>: UIViewRepresentable where T:ObservableObject, T:dataModel, V: View {
@EnvironmentObject var dataModel: T
let contentView: V // here !!
// let builder: () -> V // alternate, as view builder
func makeCoordinator() -> Coordinator {
Coordinator(self, regs: dataModel)
}
let size: CGSize
func makeUIView(context: Context) -> UIScrollView {
let scrollView = UIScrollView()
scrollView.refreshControl = UIRefreshControl()
scrollView.refreshControl?.addTarget(context.coordinator, action: #selector(Coordinator.handleRefreshControl(sender:)), for: .valueChanged)
let refreshVC = UIHostingController(rootView: self.contentView) // here !!
...
Update: simplifying to avoid missed parts, here is a demo of usage
struct DemoView: View {
var body: some View {
GenericRefreshView(dataModel: TestViewModel(),
contentView: Text("Demo"),
size: CGSize(width: 100, height: 100))
}
}
struct GenericRefreshView<T, V>: UIViewRepresentable where T:ObservableObject, V: View {
@ObservedObject var dataModel: T // << EnvironmentObject is internal, so cannot be inferred from input arguments, so not usable here
let contentView: V
let size: CGSize
Related Topics
How to Calculate Actual Font Point Size in iOS 7 (Not the Bounding Rectangle)
Create and Perform Segue Without Storyboards
Eraser Not Working in iOS Drawing
How to Programmatically Get iOS Status Bar Height
Avplayer Stops Playing and Doesn't Resume Again
Steps to Create and Edit a Plist File in Xcode
Disable Magnification Gesture in Wkwebview
Uiview's Border Color in Interface Builder Doesn't Work
Code Signing Error: Application Failed Codesign Verification
Have a Variable with Multiple Types in Swift
Convert Request Function to Generic Type
"Too Many Symbol Files" After Successfully Submitting My Apps
iPhone Sdk Internet Connection Detection
How to Use Speech Recognition Inside the iOS Sdk
Difference Between Static Function and Singleton Class in Swift