I've got a ScrollView containing a TabView for my iOS app. I've added the .refreshable modifier to the ScrollView which contains a call to reload my data. My minimum target version is iOS 16.4, and I'm attempting to test on an iPhone SE 2020 running iOS 17.2. So far, pulling down to refresh does absolutely nothing at all.
Here is the code for the view:
import SwiftUI
struct SundayView: View {
init() {
UIScrollView.appearance().bounces = false
}
@ObservedObject var dataModel = DataModel.shared
var dataFetcher = DataFetcher()
@State private var selection = 20
var body: some View {
GeometryReader { geometry in
VStack(spacing: 10) {
Text("\(dataModel.sundaysFinal[selection/2].date) - \(selection % 2 == 0 ? "AM" : "PM")")
.font(.system(size: 23))
.foregroundStyle(Color.primary)
.padding(EdgeInsets(top: 8, leading: 0, bottom: 0, trailing: 0))
ScrollView (showsIndicators: false) {
TabView(selection: $selection) {
ForEach(Array(dataModel.sundaysFinal.enumerated()), id: \.element) { index, element in
ForEach (0..<2) { innerIndex in
Group {
if innerIndex % 2 == 0 {
SundayAMView(
preacher: element.amPreacher,
pianist: element.amPianist,
audioTechnician: element.amAudioTechnician,
videoTechnician: element.amVideoTechnician,
lockingUp: element.amLock,
teas: element.teas,
openHouse: element.openHouse
)
.tag(index * 2 + innerIndex)
}
else {
SundayPMView(
preacher: element.pmPreacher,
pianist: element.pmPianist,
audioTechnician: element.pmAudioTechnician,
videoTechnician: element.pmVideoTechnician,
lockingUp: element.pmLock
)
.tag(index * 2 + innerIndex)
}
}
}
}
}
.tabViewStyle(PageTabViewStyle())
.frame(height: max(geometry.size.height - 46, 600))
}
.refreshable {
await dataFetcher.reload()
}
}
}
}
}
Here is the data fetcher:
import Foundation
class DataFetcher {
var dataModel = DataModel.shared
let outDateFormatter: DateFormatter = {
let df = DateFormatter()
df.dateFormat = "yyyyMMdd"
return df
}()
init() {
let thisSunday = Date.today().next(.sunday, considerToday: true)
let startDate = thisSunday.addWeek(noOfWeeks: -10)
let endDate = thisSunday.addWeek(noOfWeeks: 10)
let startDateString = outDateFormatter.string(from: startDate)
let endDateString = outDateFormatter.string(from: endDate)
fetch(startDate: startDateString, endDate: endDateString)
}
func reload() async {
dataModel.resetData()
let thisSunday = Date.today().next(.sunday, considerToday: true)
let startDate = thisSunday.addWeek(noOfWeeks: -10)
let endDate = thisSunday.addWeek(noOfWeeks: 10)
let startDateString = outDateFormatter.string(from: startDate)
let endDateString = outDateFormatter.string(from: endDate)
fetch(startDate: startDateString, endDate: endDateString)
}
func fetch(startDate: String, endDate: String) {
let endpoint: Endpoint = Endpoint(
queryItems: [
URLQueryItem(name: "startdate", value: startDate),
URLQueryItem(name: "enddate", value: endDate)
]
)
guard let url = endpoint.url else { fatalError("Missing URL") }
let urlRequest = URLRequest(url: url)
let dataTask = URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in
if let error = error {
print("Request error: ", error)
return
}
guard let response = response as? HTTPURLResponse else { return }
if response.statusCode == 200 {
guard let data = data else { return }
DispatchQueue.main.async {
do {
let decodedSundays = try JSONDecoder().decode([Sunday].self, from: data)
self.dataModel.sundays = decodedSundays
//print("Data: ", decodedSundays)
self.dataModel.createSundaysFinal()
} catch let error {
print("Error decoding: ", error)
}
}
}
}
dataTask.resume()
}
}
struct Endpoint {
let queryItems: [URLQueryItem]
}
extension Endpoint {
// We still have to keep 'url' as an optional, since we're
// dealing with dynamic components that could be invalid.
var url: URL? {
var components = URLComponents()
components.scheme = "https"
components.host = "I'm keeping this private"
components.path = "I'm keeping this private"
components.queryItems = queryItems
return components.url
}
}
extension Date {
static func today() -> Date {
return Date()
}
func next(_ weekday: Weekday, considerToday: Bool = false) -> Date {
return get(.next,
weekday,
considerToday: considerToday)
}
func previous(_ weekday: Weekday, considerToday: Bool = false) -> Date {
return get(.previous,
weekday,
considerToday: considerToday)
}
func get(_ direction: SearchDirection,
_ weekDay: Weekday,
considerToday consider: Bool = false) -> Date {
let dayName = weekDay.rawValue
let weekdaysName = getWeekDaysInEnglish().map { $0.lowercased() }
assert(weekdaysName.contains(dayName), "weekday symbol should be in form \(weekdaysName)")
let searchWeekdayIndex = weekdaysName.firstIndex(of: dayName)! + 1
let calendar = Calendar(identifier: .gregorian)
if consider && calendar.component(.weekday, from: self) == searchWeekdayIndex {
return self
}
var nextDateComponent = calendar.dateComponents([.hour, .minute, .second], from: self)
nextDateComponent.weekday = searchWeekdayIndex
let date = calendar.nextDate(after: self,
matching: nextDateComponent,
matchingPolicy: .nextTime,
direction: direction.calendarSearchDirection)
return date!
}
func addWeek(noOfWeeks: Int) -> Date {
return Calendar.current.date(byAdding: .weekOfYear, value: noOfWeeks, to: self)!
}
}
// MARK: Helper methods
extension Date {
func getWeekDaysInEnglish() -> [String] {
var calendar = Calendar(identifier: .gregorian)
calendar.locale = Locale(identifier: "en_US_POSIX")
return calendar.weekdaySymbols
}
enum Weekday: String {
case monday, tuesday, wednesday, thursday, friday, saturday, sunday
}
enum SearchDirection {
case next
case previous
var calendarSearchDirection: Calendar.SearchDirection {
switch self {
case .next:
return .forward
case .previous:
return .backward
}
}
}
}
To be honest, I'm not sure what to do next as this apparently just works out of the box for iOS 16+.
It turns out I needed to set
UIScrollView.appearance().bounces = false
to
UIScrollView.appearance().bounces = true