Search code examples
iosswiftuiscrollview

SwiftUI ScrollView pull to refresh not working


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+.


Solution

  • It turns out I needed to set

    UIScrollView.appearance().bounces = false
    

    to

    UIScrollView.appearance().bounces = true