{"id":3668,"date":"2026-01-02T11:37:40","date_gmt":"2026-01-02T02:37:40","guid":{"rendered":"https:\/\/www.freelifemakers.org\/wordpress\/?p=3668"},"modified":"2026-02-20T09:32:39","modified_gmt":"2026-02-20T00:32:39","slug":"swiftrandom-user-api-horizaontal-infinite-scroll","status":"publish","type":"post","link":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/2026\/01\/02\/swiftrandom-user-api-horizaontal-infinite-scroll\/","title":{"rendered":"[Swift]\ub79c\ub364\uc720\uc800API\uac00\ub85c \ubb34\ud55c\uc2a4\ud06c\ub864\/Random User API Horizontal Infinite Scroll Example"},"content":{"rendered":"\n<p>\ud83d\udc49\ud83c\udffb Randomuser API\ub97c \uc0ac\uc6a9\ud574\uc11c \uac00\ub85c \ubb34\ud55c \uc2a4\ud06c\ub864\uc744 \uad6c\ud604\ud55c \ucef4\ud3ec\ub10c\ud2b8 \uc785\ub2c8\ub2e4.<br>This is a component that implements horizontal infinite scrolling using the Randomuser API.<\/p>\n\n\n\n<p>\ud83d\udc49\ud83c\udffb \ucf54\ub4dc\uc124\uba85\uc740 \uc8fc\uc11d\uc744 \ucc38\uc870\ud558\uc138\uc694 \uadf8\ub9ac\uace0 \ubd80\uc871\ud55c \uc124\uba85\uc740 \uc544\ub798\uc758 \ud3ec\uc2a4\ud2b8\ub97c \ucc38\uc870\ud558\uc2dc\uba74 \ub429\ub2c8\ub2e4.<br>Please refer to the comments for code explanations, and for any missing explanations, please refer to the post below.<\/p>\n\n\n\n<figure class=\"wp-block-embed is-type-wp-embed is-provider-freelifemakers-org wp-block-embed-freelifemakers-org\"><div class=\"wp-block-embed__wrapper\">\n<blockquote class=\"wp-embedded-content\" data-secret=\"e4BGFXn2Tz\"><a href=\"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/2025\/12\/31\/swift-protocols\/\">[Swift]Identifiable, Equatable, Hashable Protocols<\/a><\/blockquote><iframe loading=\"lazy\" class=\"wp-embedded-content\" sandbox=\"allow-scripts\" security=\"restricted\" style=\"position: absolute; visibility: hidden;\" title=\"&#8220;[Swift]Identifiable, Equatable, Hashable Protocols&#8221; &#8212; freelifemakers.org\" src=\"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/2025\/12\/31\/swift-protocols\/embed\/#?secret=lWheeh6Puz#?secret=e4BGFXn2Tz\" data-secret=\"e4BGFXn2Tz\" width=\"560\" height=\"315\" frameborder=\"0\" marginwidth=\"0\" marginheight=\"0\" scrolling=\"no\"><\/iframe>\n<\/div><\/figure>\n\n\n\n<p>\ud83d\udc49\ud83c\udffb \ucf54\ub4dc \/ Code<\/p>\n\n\n\n<p>\u2714\ufe0fContentView.swift<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\nimport SwiftUI\n\nstruct ContentView: View {\n    var body: some View {\n        NavigationStack {\n            ScrollView {\n                VStack(spacing: 30) {\n                    \n                    \/\/ \uc139\uc158 1: \uc778\uae30 \uc0ac\uc6a9\uc790\n                    \/\/ Section 1: Popular Users\n                    HorizontalUserScrollView(\n                        title: \"\uc778\uae30 \uc0ac\uc6a9\uc790\",\n                        url:\"https:\/\/randomuser.me\/api\/?results=20\",\n                        viewModel: RandomUserViewModel()\n                    )\n                    \n                    \/\/ \uc139\uc158 2: \uc0c8\ub85c\uc6b4 \uc0ac\uc6a9\uc790\n                    \/\/ Section 2: New Users\n                    HorizontalUserScrollView(\n                        title: \"\uc0c8\ub85c\uc6b4 \uc0ac\uc6a9\uc790\",\n                        url:\"https:\/\/randomuser.me\/api\/?results=20\",\n                        viewModel: RandomUserViewModel()\n                    )\n                    \n                    \/\/ \uc139\uc158 3: \ucd94\ucc9c \uc0ac\uc6a9\uc790\n                    \/\/ Section 3: Recommended Users\n                    HorizontalUserScrollView(\n                        title: \"\uc624\ub298\uc758 \ucd94\ucc9c\",\n                        url:\"https:\/\/randomuser.me\/api\/?results=20\",\n                        viewModel: RandomUserViewModel()\n                    )\n\n                }\n                .padding(.vertical)\n            }\n            .navigationTitle(\"Horizontal Infinite Scroll\")\n            .refreshable {\n                \/\/ \uc804\uccb4 \uc0c8\ub85c\uace0\uce68 \uc6d0\ud558\uba74 \uc5ec\uae30\uc11c \ubaa8\ub4e0 ViewModel \ub9ac\ud504\ub808\uc2dc\n                \/\/ \ud604\uc7ac\ub294 \uac01 \uc139\uc158\uc774 \ub3c5\ub9bd\uc801\uc774\ubbc0\ub85c pull-to-refresh\ub294 \uc0dd\ub7b5\ud558\uac70\ub098 \ucee4\uc2a4\ud140 \ucc98\ub9ac\n                \n                \/\/ If you want a full refresh, refresh all ViewModels here.\n                \/\/ Currently, each section is independent, so either omit pull-to-refresh or handle it customally.\n            }\n        }\n    }\n}\n\n#Preview {\n    ContentView()\n}\n<\/code><\/pre>\n\n\n\n<p>&nbsp;<\/p>\n\n\n\n<p>\u2714\ufe0f HorizaontalUserScrollView<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import SwiftUI\n\n\/\/ \uac00\ub85c \ubb34\ud55c \uc2a4\ud06c\ub864 \ucef4\ud3ec\ub10c\ud2b8\n\/\/ Horizontal infinite scroll component\nstruct HorizontalUserScrollView: View {\n    let title: String\n    let url: String\n    @StateObject var viewModel: RandomUserViewModel\n    \n    var body: some View {\n        VStack(alignment: .leading) {\n            \/\/ \uc139\uc158 \uc81c\ubaa9 \/ section title\n            Text(title)\n                .font(.title2)\n                .fontWeight(.bold)\n                .padding(.horizontal)\n            \n            ScrollView(.horizontal, showsIndicators: false) {\n                LazyHStack(spacing: 20) {\n                    ForEach(viewModel.users) { user in\n                        VStack {\n                            Text(\"\\(user.name.first) \\(user.name.last)\")\n                                .font(.headline)\n                                .multilineTextAlignment(.center)\n                            \n                            AsyncImage(url: URL(string: user.picture.large)) { phase in\n                                switch phase {\n                                case .success(let image):\n                                    image\n                                        .resizable()\n                                        .scaledToFit()\n                                        .frame(width: 120, height: 120)\n                                        .clipShape(Circle())\n                                case .empty, .failure:\n                                    ProgressView()\n                                        .frame(width: 120, height: 120)\n                                @unknown default:\n                                    EmptyView()\n                                }\n                            }\n                        }\n                        .frame(width: 160)\n                        .padding()\n                        .background(Color(.secondarySystemBackground))\n                        .cornerRadius(16)\n                        .shadow(radius: 3)\n                        \/\/ \ub9c8\uc9c0\ub9c9 \uc0ac\uc6a9\uc790 \uce74\ub4dc\uac00 \ub098\ud0c0\ub0a0 \ub54c \ucd94\uac00 \ub85c\ub4dc\n                        \/\/ Load additional cards when the last user card appears\n                        .onAppear {\n                            if user == viewModel.users.last {\n                                viewModel.fetchMoreUsers(url:url)  \/\/ \ucd94\uac00 \ub85c\ub4dc (append)\n                                \/\/ viewModel.fetchMoreUsers(url: \"https:\/\/randomuser.me\/api\/?results=20\")  \/\/ \ucd94\uac00 \ub85c\ub4dc (append)\n                            }\n                        }\n                    }\n                    \n                    \/\/ \ub85c\ub529 \uc778\ub514\ucf00\uc774\ud130 \/ loading indicator\n                    if viewModel.isLoadingMore {\n                        ProgressView(\"\ubd88\ub7ec\uc624\ub294 \uc911\u2026\")\n                            .frame(width: 160, height: 240)\n                            .padding()\n                    }\n                }\n                .padding(.horizontal)\n            }\n        }\n        \/\/ \uc139\uc158\uc774 \ud654\uba74\uc5d0 \ub098\ud0c0\ub0a0 \ub54c \ucd08\uae30 \ub370\uc774\ud130 \ub85c\ub4dc (\ud55c \ubc88\ub9cc!)\n        \/\/ Load initial data when the section appears on screen (only once!)\n        .onAppear {\n            if viewModel.users.isEmpty {\n                Task {\n                    \/\/ \uc804\uccb4 \uad50\uccb4 (\ucd08\uae30 \ub85c\ub4dc \uc804\uc6a9)\n                    \/\/ Full replacement (initial load only)\n                    await viewModel.loadInitialUsers(url:url)\n                    \/\/ await viewModel.loadInitialUsers(url:\"https:\/\/randomuser.me\/api\/?results=20\")  \/\/ \uc804\uccb4 \uad50\uccb4 (\ucd08\uae30 \ub85c\ub4dc \uc804\uc6a9)\n                }\n            }\n        }\n    }\n}\n<\/code><\/pre>\n\n\n\n<p>\u2714\ufe0f RandomUserModel<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import Foundation\n\nstruct RandomUserResponse: Codable {\n    let results: &#91;RandomUser]\n}\n\nstruct RandomUser: Codable, Identifiable, Equatable {\n\n    let login: Login\n    \n    let name: Name\n    let picture: Picture\n    \n    \/\/ Identifiable\n    var id: String { login.uuid }\n    \n    \/\/ MARK: - \ub0b4\ubd80 \uad6c\uc870\uccb4\n    struct Login: Codable, Equatable {\n        let uuid: String\n        \/\/ \ud544\uc694\ud558\uba74 username, password \ub4f1 \ucd94\uac00 \uac00\ub2a5\n        \/\/ You can add username, password, etc. if needed.\n    }\n    \n    struct Name: Codable, Equatable {\n        let title: String?   \/\/ \"Mr\", \"Mrs\" ...\n        let first: String\n        let last: String\n    }\n    \n    struct Picture: Codable, Equatable {\n        let large: String\n        let medium: String?\n        let thumbnail: String?\n    }\n}<\/code><\/pre>\n\n\n\n<p>\u2714\ufe0f RandomUserRowInfiniteApp<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import SwiftUI\n\n@main\nstruct RandomUserRowInfiniteApp: App {\n    var body: some Scene {\n        WindowGroup {\n            ContentView()\n        }\n    }\n}<\/code><\/pre>\n\n\n\n<p>\u2714\ufe0f RandomUserViewModel<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\nimport Foundation\nimport Combine\n\n@MainActor\nclass RandomUserViewModel: ObservableObject {\n    @Published var users: &#91;RandomUser] = &#91;]\n    @Published var errorMessage: String?\n    @Published var isLoadingMore = false\n    \n    private var debounceTask: Task&lt;Void, Never&gt;?\n    \n    \/\/ \ucc98\uc74c \ub370\uc774\ud130 \ub85c\ub4dc\uc6a9 (\ubdf0\uac00 \ub098\ud0c0\ub0a0 \ub54c \ud638\ucd9c)\n    \/\/ For initial data load (called when the view appears)\n    func loadInitialUsers(url: String = \"https:\/\/randomuser.me\/api\/?results=20\") async {\n        guard users.isEmpty else { return } \/\/ \uc774\ubbf8 \ub370\uc774\ud130 \uc788\uc73c\uba74 \uc911\ubcf5 \ub85c\ub4dc \ubc29\uc9c0\n        \n        isLoadingMore = true\n        await fetchUsers(from: url, append: false)\n        isLoadingMore = false\n    }\n    \n    \/\/ \uc591\ucabd \ub05d\uc5d0\uc11c \ucd94\uac00 \ub85c\ub4dc (\uc88c\uc6b0 \ubb34\ud55c \uc2a4\ud06c\ub864\uc6a9)\n    \/\/ Additional loads from both ends (for infinite left and right scrolling)\n    func fetchMoreUsers(url: String = \"https:\/\/randomuser.me\/api\/?results=10\") {\n        debounceTask?.cancel()\n        \n        debounceTask = Task {\n            try? await Task.sleep(nanoseconds: 300_000_000) \/\/ 0.3\ucd08 \ub514\ubc14\uc6b4\uc2a4 \/0.3 second debounce\n            \n            guard !isLoadingMore else { return }\n            isLoadingMore = true\n            \n            await fetchUsers(from: url, append: true)\n            \n            isLoadingMore = false\n        }\n    }\n    \n    \/\/ \uacf5\ud1b5 \ub124\ud2b8\uc6cc\ud06c \uba54\uc11c\ub4dc (\uc774\uc804\uacfc \ub3d9\uc77c) \/\n    \/\/ Common network methods (same as before)\n    private func fetchUsers(from urlString: String, append: Bool) async {\n        guard let url = URL(string: urlString) else {\n            self.errorMessage = \"\uc798\ubabb\ub41c URL\uc785\ub2c8\ub2e4.\"\n            return\n        }\n        \n        do {\n            let (data, _) = try await URLSession.shared.data(from: url)\n            let decoded = try JSONDecoder().decode(RandomUserResponse.self, from: data)\n            \n            if append {\n                self.users.append(contentsOf: decoded.results)\n            } else {\n                self.users = decoded.results\n            }\n        } catch {\n            self.errorMessage = error.localizedDescription\n        }\n    }\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-large\"><img loading=\"lazy\" decoding=\"async\" width=\"488\" height=\"1024\" src=\"https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/01\/Hinfinite-488x1024.png\" alt=\"\" class=\"wp-image-3676\" srcset=\"https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/01\/Hinfinite-488x1024.png 488w, https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/01\/Hinfinite-143x300.png 143w, https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/01\/Hinfinite-400x840.png 400w, https:\/\/www.freelifemakers.org\/wordpress\/wp-content\/uploads\/2026\/01\/Hinfinite.png 544w\" sizes=\"auto, (max-width: 488px) 100vw, 488px\" \/><\/figure>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>\ud83d\udc49\ud83c\udffb Randomuser API\ub97c \uc0ac\uc6a9\ud574\uc11c \uac00\ub85c \ubb34\ud55c \uc2a4\ud06c\ub864\uc744 \uad6c\ud604\ud55c \ucef4\ud3ec\ub10c\ud2b8 \uc785\ub2c8\ub2e4.This is a component that implements horizontal infinite scrolling using the Randomuser API. \ud83d\udc49\ud83c\udffb \ucf54\ub4dc\uc124\uba85\uc740 \uc8fc\uc11d\uc744 \ucc38\uc870\ud558\uc138\uc694 \uadf8\ub9ac\uace0 \ubd80\uc871\ud55c \uc124\uba85\uc740 \uc544\ub798\uc758 \ud3ec\uc2a4\ud2b8\ub97c \ucc38\uc870\ud558\uc2dc\uba74 \ub429\ub2c8\ub2e4.Please refer to the comments for code explanations, and for any missing explanations, please refer to the post below. \ud83d\udc49\ud83c\udffb \ucf54\ub4dc \/ Code [&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-3668","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\/3668","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=3668"}],"version-history":[{"count":12,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/posts\/3668\/revisions"}],"predecessor-version":[{"id":3683,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/posts\/3668\/revisions\/3683"}],"wp:attachment":[{"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/media?parent=3668"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/categories?post=3668"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.freelifemakers.org\/wordpress\/index.php\/wp-json\/wp\/v2\/tags?post=3668"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}