본문 바로가기
📱Mobile/🔥Swift

[Swift] SwiftUI Infinite ScrollView 무한스크롤뷰 데이터 추가로드

by 후누스 토르발즈 2021. 11. 1.
반응형

 

 

 

trainingApp.swift

import SwiftUI

@main
struct trainingApp: App {
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

 

ContentView.swift

import SwiftUI

struct ContentView: View {
    @State var current: Int = 0
    @State var maxCnt: Int = 0
    @State var itemList: [ItemModel] = []
    @State var hasMorePages:Bool = true
    var body: some View {
        GeometryReader { geometry in
            let columns: [GridItem] = [GridItem()]

            let width = geometry.size.width / 1.05
            let height = geometry.size.height / 2.5
            let spacing = geometry.size.width / 40

            ScrollView(.vertical, showsIndicators: false){
                ZStack{
                    LazyVGrid(columns: columns, alignment: .center, spacing: spacing) {
                        ForEach(itemList) { item in
                            Button(action: {
                                print("item.label : \(item.label)   item.id : \(item.id)")
                            }){
                                RoundedRectangle(cornerRadius: 16, style: .continuous)
                                    .fill(Color.white)
                                    .overlay(
                                        VStack{
                                            Text(item.id)
                                                .font(.system(size: 12))
                                                .foregroundColor(.black)
                                                .padding(.bottom, 10)
                                            Text(item.label)
                                                .foregroundColor(.black)
                                        }
                                    )
                                    .frame(width: width, height: height)
                                    .shadow(color: .gray, radius: 2, x: CGFloat(2), y: CGFloat(2))
                            }
                            .onAppear(){
                                let index = Int(item.label.components(separatedBy: "List Item ")[1])!
                                if(maxCnt < index){
                                    maxCnt = index
                                    if(maxCnt % 10 == 0){
                                        loadItems()
                                    }
                                }
                                
                            }
                        }
                    }
                }.onAppear(){
                    print("onAppear")
                        loadItems()
                    
                }
                .padding(.vertical, 20)
            }
        }
    }
    
    func loadItems(){
        print("loadItems")
        if(hasMorePages){
            current+=1
            let request = HttpCommon().httpCall(url: "https://s3.eu-west-2.amazonaws.com/com.donnywals.misc/feed-\(current).json", method: "GET", body: nil)
            let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
                if error != nil || data == nil {
                    print("Client error!")
                    return
                }
                if let response = response as? HTTPURLResponse {
                    let statusCode = response.statusCode
                    print("response.statusCode : ", response.statusCode)
                    if(statusCode == 401){
                        print("401  Error")
                    } else if((404...500).contains(response.statusCode)){
                        print("비정상 접근 또는 서버 에러")
                    } else if((200...299).contains(response.statusCode)){
                        do {
                            let responseModel = try JSONDecoder().decode(ResponseModel<ItemModel>.self, from:data!)
                            hasMorePages = responseModel.hasMorePages
                            let loadItems = responseModel.items
                            loadItems.forEach { loadItem in
                                itemList.append(loadItem)
                            }
                        } catch let e as NSError {
                            print("An error has occured while parsing JSON Obejt : \(e.localizedDescription)")
                        }
                    } else {
                        print("접속장애")
                    }
                }
            }
            task.resume()
        }
    }
}

 

ResponseModel.swift

class ResponseModel<T : Decodable>: Decodable {
    var items: [T]
    var hasMorePages: Bool
}

 

ListItem.swift

import SwiftUI

struct ListItem: View {
  let item: ItemModel

  var body: some View {
    ZStack(alignment: .center) {
      RoundedRectangle(cornerRadius: 5)
          .foregroundColor(.gray)
        Text("item.id \(item.id)!")
        Text("item.label \(item.label)!")
    }
    .frame(height: 72)
  }
}

 

ItemModel.swift

class ItemModel: Decodable, Identifiable {
    var id: String
    var label: String
}

 

HttpCommon.swift

import SwiftUI

class HttpCommon {
    
    init(){
        print("HttpCommon Init()")
    }
    
    
    func httpCall(url: String, method: String, body: [String: String]?) -> URLRequest {
        let Url = String(format: url)
        let serviceUrl = URL(string: Url)
        var request = URLRequest(url: serviceUrl!)
        request.httpMethod = method
        request.timeoutInterval = 20
        if(method == "POST") {
            request.httpBody = try? JSONSerialization.data(withJSONObject: body!, options: [])
        }
        print("url : ", serviceUrl!)
        return request
    }
}

 

 

초기화면

 

제일 높은 인덱스에 올랐을 경우 maxCnt에 대입하고 % 10 == 0 인 경우 로드한다.

 

스크롤 했을 경우

 

 

ForEach에서 몇번째 인덱스인지 추출하는 방법을 몰라서 List Item {number}를 포함한 data를 사용하기때문에 split해서 몇번째 인덱스인지 감지하였다.

 

LazyVGrid 를 꼭 사용해야만 가능한 스크롤 뷰이다. LazyVGrid는 수직으로 그리드에 자식 뷰를 정렬하여 필요한 만큼만 아이템을 생성하는 컨테이너 뷰이다. 때문에 그려질때마다 onAppear() 해서 가능하지만 LazyVGrid를 사용하지 않는경우에는 이미 화면에 항목을 다 그려버려서 onAppear()가 이미 실행되고 끝나버린다. 

 

는 현재 화면에 표시되야 되는것을 감지하기 보여준다.

원래는 index % 10 == 0 인경우에 로드하였으나 위쪽으로 다시 올라가는경우, 예를들면 최초 10을 로드하고

20, 30까지 로드 후 다시 20 으로 가는경우 20 % 10 == 0 이 되기때문에 20번째에 위치해 있음에도 불구하고 40까지 로드해버린다.

이 때문에 maxCnt를 계속 대입해서 다시 위로 스크롤해서 올라가는것들에는 호출안되도록 막았다.

 

 

 

위치를 감지해서 로드하는것이 아니고 현재 생성된 데이터의 number를 위치로 보기 때문에 사용에 제한적이다.

 

반응형