{"id":3440,"date":"2025-12-25T19:53:46","date_gmt":"2025-12-25T10:53:46","guid":{"rendered":"https:\/\/www.freelifemakers.org\/wordpress\/?p=3440"},"modified":"2025-12-25T20:19:24","modified_gmt":"2025-12-25T11:19:24","slug":"swift-infinitescreoo-refresh-loading","status":"publish","type":"post","link":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/2025\/12\/25\/swift-infinitescreoo-refresh-loading\/","title":{"rendered":"[swift]RandomUser Api &#8211; \ubb34\ud55c\uc2a4\ud06c\ub864,\uc0c8\ub85c\uace0\uce68 \ub85c\ub529\/Infinite Scroll,refresh loading"},"content":{"rendered":"\n<p>\ud83d\udc49\ud83c\udffb \uc544\ub798\ub294 RandomUser API\ub97c \ud65c\uc6a9\ud55c \ubb34\ud55c \uc2a4\ud06c\ub864\uc744 \uad6c\ud604\ud55c \ub0b4\uc6a9\uc785\ub2c8\ub2e4.<br>Below is an implementation of infinite scrolling using the RandomUser API.<\/p>\n\n\n\n<p>\ud83d\udc49\ud83c\udffb@MainActor \ub97c \uc0ac\uc6a9\ud558\ub294\uc774\uc720<br>Why use @MainActor<\/p>\n\n\n\n<p>SwiftUI UI \uc5c5\ub370\uc774\ud2b8\ub294 \ubc18\ub4dc\uc2dc \uba54\uc778 \uc2a4\ub808\ub4dc\uc5d0\uc11c \uc774\ub904\uc838\uc57c \ud569\ub2c8\ub2e4.<br>SwiftUI UI updates must always be done on the main thread.<\/p>\n\n\n\n<p>\u00a0@Published\ub85c \uc120\uc5b8\ub41c \uac12\ub4e4\uc774 \ubcc0\uacbd\ub418\uba74 UI\ub97c \uc5c5\ub370\uc774\ud2b8\ud569\ub2c8\ub2e4.<br>Updates the UI when values \u200b\u200bdeclared with @Published change.<\/p>\n\n\n\n<p>\u00a0\ubdf0\uc758 \uc5c5\ub370\uc774\ud2b8\uac00 \ubc31\uadf8\ub77c\uc6b4\ub4dc \uc2a4\ub808\ub4dc\uc5d0\uc11c \ubcc0\uacbd\ub418\uba74 \ub7f0\ud0c0\uc784 \uacbd\uace0 + UI \ubc84\uadf8 \ubc1c\uc0dd \uac00\ub2a5\ud569\ub2c8\ub2e4.<br>If the view&#8217;s update is made on a background thread, runtime warnings + UI bugs may occur.<\/p>\n\n\n\n<p>\u00a0@MainActor\ub97c \ud074\ub798\uc2a4\uc5d0 \ubd99\uc774\uba74 \uc774 \ud074\ub798\uc2a4 \uc548\uc758 \ubaa8\ub4e0 \ud504\ub85c\ud37c\ud2f0 \uc811\uadfc\uacfc \ubcc0\uacbd\uc774 \uc790\ub3d9\uc73c\ub85c \uba54\uc778 \uc2a4\ub808\ub4dc\uc5d0\uc11c \uc2e4\ud589\ub429\ub2c8\ub2e4.<br>When you annotate a class with @MainActor, all property accesses and changes within that class will automatically be executed on the main thread.<\/p>\n\n\n\n<p>\ud83d\udc49\ud83c\udffb \ub2e4\ub978 \ub0b4\uc6a9\ub4e4\uc740 \uc774\uc804 \ud3ec\uc2a4\ud2b8\ub0b4\uc6a9\uacfc \ucf54\ub4dc \uc8fc\uc11d\uc744 \ucc38\uc870 \ud558\uc2dc\uba74 \ub420 \uac83 \uac19\uc2b5\ub2c8\ub2e4.<br>For other details, please refer to the previous post and code comments.<\/p>\n\n\n\n<p>\ud83d\udc49\ud83c\udffb \ucf54\ub4dc \/ Code<\/p>\n\n\n\n<p>\u2714\ufe0fContentView<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import SwiftUI\n\nstruct ContentView: View {\n    \n    @StateObject private var viewModel = RandomUserViewModel()\n    \n    var body: some View {\n        ScrollView {\n            LazyVStack(spacing: 20) {\n                \n                ForEach(viewModel.users.indices, id: \\.self) { index in\n                    let user = viewModel.users&#91;index]\n                    \n                    VStack {\n                        \/\/ \uc720\uc800\ub124\uc784\n                        Text(\"\\(user.name.first) \\(user.name.last)\")\n                            .font(.headline)\n                        \n                        \/\/ \uc774\ubbf8\uc9c0\n                        AsyncImage(url: URL(string: user.picture.large)) { image in\n                            image.resizable()\n                                .scaledToFit()\n                                .frame(width: 140, height: 140)\n                                .clipShape(Circle())\n                        } placeholder: {\n                            ProgressView()\n                        }\n                    }\n                    .onAppear { \/\/ \ubb34\ud55c \uc2a4\ud06c\ub864 \uac10\uc9c0\n                        \/\/ \ub9c8\uc9c0\ub9c9 \uc140\uc5d0\uc11c \ubb34\ud55c\uc2a4\ud06c\ub864 \ud638\ucd9c\n                        if index == viewModel.users.count - 1 {\n                            viewModel.fetchMoreUsers()\n                        }\n                    }\n                }\/\/ ForEach\n                \n                \/\/ @Published\n                if viewModel.isLoadingMore {\n                    ProgressView(\"\ubd88\ub7ec\uc624\ub294 \uc911\u2026 \/ Loading...\")\n                        .padding()\n                }\n                \n            }\n            .padding()\n        }\n        .refreshable {\n            \/\/ \uc0c8\ub85c\uace0\uce68 \/ refresh\n            await viewModel.refreshUsers()\n        }\n        .onAppear {\n            \/\/ \ucd5c\ucd08 \ub370\uc774\ud130 \ub85c\ub4dc \/ First data load\n            if viewModel.users.isEmpty {\n                viewModel.fetchMoreUsers()\n            }\n        }\n    }  \n\n}\n\n#Preview {\n    ContentView()\n}\n<\/code><\/pre>\n\n\n\n<p>\u2714\ufe0fRandomUserInfiniteApp<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import SwiftUI\n\n@main\nstruct RandomUserInfiniteApp: App {\n    var body: some Scene {\n        WindowGroup {\n            ContentView()\n        }\n    }\n}\n<\/code><\/pre>\n\n\n\n<p>\u2714\ufe0fRandomUserModel<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import Foundation\n\n\/\/ Codable\uc744 \uc4f0\uba74 JSON \u2194 Struct \ubcc0\ud658\uc774 \uc790\ub3d9\n\/\/ If you use Codable, JSON \u2194 Struct conversion is automatic.\n\nstruct RandomUserResponse: Codable { \n    let results: &#91;RandomUser]\n}\n\nstruct RandomUser: Codable {\n    let name: Name\n    let picture: Picture\n    \n    struct Name: Codable {\n        let first: String\n        let last: String\n    }\n    \n    struct Picture: Codable {\n        let large: String\n    }\n}<\/code><\/pre>\n\n\n\n<p>\u2714\ufe0fRandomUserViewModel<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import Foundation\nimport Combine\n\n@MainActor\nclass RandomUserViewModel: ObservableObject {\n    @Published var users: &#91;RandomUser] = &#91;]\n    @Published var errorMessage: String?\n    \n    @Published var isLoadingMore = false       \/\/ \ubb34\ud55c\uc2a4\ud06c\ub864 \ub85c\ub529 \/ infinite scroll loading\n    @Published var isRefreshing = false        \/\/ \uc0c8\ub85c\uace0\uce68 \ub85c\ub529 \/ refresh loading\n    \n    private var debounceTask: Task&lt;Void, Never&gt;?   \/\/ \ubb34\ud55c\uc2a4\ud06c\ub864 \ub514\ubc14\uc6b4\uc2a4\uc6a9 \/ For infinite scroll debounce\n    \n    \/\/ MARK: - \ubb34\ud55c\uc2a4\ud06c\ub864 \/ infinite scroll\n    func fetchMoreUsers() {\n        \n        \/\/ \uc774\ubbf8 \uc0c8\ub85c\uace0\uce68 \uc911\uc774\uba74 \ubb34\ud55c\uc2a4\ud06c\ub864 \uae08\uc9c0 \n        \/\/ Disable infinite scrolling if already refreshing\n\n        guard !isRefreshing else { return }\n        \n        \/\/ \ub514\ubc14\uc6b4\uc2f1: \uae30\uc874 \uc791\uc5c5 \ucde8\uc18c\n        \/\/ Debounce: Cancel existing work\n\n        debounceTask?.cancel()\n        \n        debounceTask = Task {\n            try? await Task.sleep(nanoseconds: 400_000_000) \/\/ 0.4\ucd08 \ub514\ubc14\uc6b4\uc2a4 \/ 0.4 second debounce\n            \n            \/\/ \uc774\ubbf8 \ub85c\ub529 \uc911\uc774\ub77c\uba74(fetch \uc911\uc774\uba74) \ud568\uc218 \uc2e4\ud589\ud558\uc9c0 \ub9d0\uace0 \ubc14\ub85c \uc885\ub8cc\ud574\ub77c.\n            \/\/ If it is already loading (fetching), \n            \/\/do not execute the function and exit immediately.\n\n            guard !isLoadingMore else { return }\n            isLoadingMore = true\n            \n            do {\n                 \/\/ \uc808\ub300 nil\uc774 \uc544\ub2cc\uacbd\uc6b0\n                 \/\/ if it is never nil\n\n                let url = URL(string: \"https:\/\/randomuser.me\/api\/?results=10\")!  \n                let (data, _) = try await URLSession.shared.data(from: url)\n                let decoded = try JSONDecoder().decode(RandomUserResponse.self, from: data)\n                \n                self.users.append(contentsOf: decoded.results)\n                self.isLoadingMore = false\n                \n            } catch {\n                self.errorMessage = error.localizedDescription\n                self.isLoadingMore = false\n            }\n        }\n    }\n    \n    \n    \/\/ MARK: - \uc0c8\ub85c\uace0\uce68 \/ refresh\n    func refreshUsers() async {\n\n        guard !isLoadingMore else { return }\n\n        do {\n            let url = URL(string: \"https:\/\/randomuser.me\/api\/?results=20\")!\n            let (data, _) = try await URLSession.shared.data(from: url)\n            let decoded = try JSONDecoder().decode(RandomUserResponse.self, from: data)\n\n            self.users.removeAll()\n            self.users = decoded.results\n            self.isRefreshing = false \n\n        } catch {\n            self.errorMessage = error.localizedDescription\n            self.isRefreshing = false \n        }\n    }<\/code><\/pre>\n\n\n\n<p>\ud83d\udc49\ud83c\udffb \uc2a4\ud06c\ub9b0 \uc0f7 \/ ScreenShot<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"408\" height=\"868\" src=\"https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2025\/12\/\uc2a4\ud06c\ub9b0\uc0f7-2025-12-25-\uc624\ud6c4-8.08.17.png\" alt=\"\" class=\"wp-image-3457\" srcset=\"https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2025\/12\/\uc2a4\ud06c\ub9b0\uc0f7-2025-12-25-\uc624\ud6c4-8.08.17.png 408w, https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2025\/12\/\uc2a4\ud06c\ub9b0\uc0f7-2025-12-25-\uc624\ud6c4-8.08.17-141x300.png 141w, https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2025\/12\/\uc2a4\ud06c\ub9b0\uc0f7-2025-12-25-\uc624\ud6c4-8.08.17-400x851.png 400w\" sizes=\"auto, (max-width: 408px) 100vw, 408px\" \/><\/figure>\n","protected":false},"excerpt":{"rendered":"<p>\ud83d\udc49\ud83c\udffb \uc544\ub798\ub294 RandomUser API\ub97c \ud65c\uc6a9\ud55c \ubb34\ud55c \uc2a4\ud06c\ub864\uc744 \uad6c\ud604\ud55c \ub0b4\uc6a9\uc785\ub2c8\ub2e4.Below is an implementation of infinite scrolling using the RandomUser API. \ud83d\udc49\ud83c\udffb@MainActor \ub97c \uc0ac\uc6a9\ud558\ub294\uc774\uc720Why use @MainActor SwiftUI UI \uc5c5\ub370\uc774\ud2b8\ub294 \ubc18\ub4dc\uc2dc \uba54\uc778 \uc2a4\ub808\ub4dc\uc5d0\uc11c \uc774\ub904\uc838\uc57c \ud569\ub2c8\ub2e4.SwiftUI UI updates must always be done on the main thread. \u00a0@Published\ub85c \uc120\uc5b8\ub41c \uac12\ub4e4\uc774 \ubcc0\uacbd\ub418\uba74 UI\ub97c \uc5c5\ub370\uc774\ud2b8\ud569\ub2c8\ub2e4.Updates the UI when values \u200b\u200bdeclared with @Published [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[19,1],"tags":[],"class_list":["post-3440","post","type-post","status-publish","format-standard","hentry","category-swift","category-uncategorized","missing-thumbnail"],"_links":{"self":[{"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/posts\/3440","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/comments?post=3440"}],"version-history":[{"count":22,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/posts\/3440\/revisions"}],"predecessor-version":[{"id":3465,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/posts\/3440\/revisions\/3465"}],"wp:attachment":[{"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/media?parent=3440"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/categories?post=3440"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/tags?post=3440"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}