[swift]RandomUser.me API

๐Ÿ‘‰๐Ÿป ์•„๋ž˜๋Š” RandomUser.me์˜ API๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์œ ์ €๋ฅผ ์•ฑ์—์„œ ๋žœ๋คํ•˜๊ฒŒ ํ˜ธ์ถœํ•˜๋Š” ๋‚ด์šฉ์ž…๋‹ˆ๋‹ค.
Below is how to randomly call a user in your app using RandomUser.me’s API.

๐Ÿ‘‰๐Ÿป ์„œ๋ฒ„๋ฅผ ๋”ฐ๋กœ ๊ตฌ์„ฑํ•˜์ง€์•Š๊ณ  api๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์•ฑ์„ ๋งŒ๋“ค์–ด ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
You can create an app using the API without configuring a separate server.

๐Ÿ‘‰๐ŸปRandomUser.me Api

โœ”๏ธ ๋ธŒ๋ผ์šฐ์ €์—์„œ https://randomuser.me/api ์ด๋ ‡๊ฒŒ ์‹คํ–‰์‹œ ์‚ฌ์šฉ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
You can use it by running https://randomuser.me/api in your browser.

โœ”๏ธ ๋ฐ์ดํ„ฐ๋ฅผ ์ƒˆ๋กญ๊ฒŒ ์š”์ฒญ์‹œ ๋žœ๋ค์œผ๋กœ ์•„๋ž˜์˜ api์ •๋ณด๊ฐ€ ๋ฐ”๋€๋‹ˆ๋‹ค.
When you request new data, the API information below changes randomly.

โญ๏ธ ์ฝ”๋“œ ์„ค๋ช…์€ ์ฃผ์„์„ ์ฐธ์กฐํ•˜์‹œ๋ฉด๋ฉ๋‹ˆ๋‹ค. ์ถ”๊ฐ€ ์„ค๋ช…์ด ํ•„์š”ํ•˜๋ฉด ๋”ฐ๋กœ ํฌ์ŠคํŒ…ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.
Please refer to the comments for code explanations. If further explanation is needed, I will post it separately.

{
  "results": [
    {
      "gender": "female",
      "name": {
        "title": "Mrs",
        "first": "Lisa",
        "last": "Fowler"
      },
      "location": {
        "street": {
          "number": 2580,
          "name": "Oaks Cross"
        },
        "city": "Armagh",
        "state": "Highlands and Islands",
        "country": "United Kingdom",
        "postcode": "HH16 2NA",
        "coordinates": {
          "latitude": "-10.3689",
          "longitude": "101.2611"
        },
        "timezone": {
          "offset": "+5:00",
          "description": "Ekaterinburg, Islamabad, Karachi, Tashkent"
        }
      },
      "email": "lisa.fowler@example.com",
      "login": {
        "uuid": "a46a4808-b2fd-452b-b923-212f144a5349",
        "username": "browndog164",
        "password": "bones",
        "salt": "k8ZwiUoK",
        "md5": "bc71f9e4f2a4960d2e309be7b51c1f31",
        "sha1": "ff2dde7c2e1d208e8ad05e06f5324d2d849fed0a",
        "sha256": "ee02e23c14550ae433bf8052c1d7d23fb7cd8b4fd42cf10a0900ef3e6c9c4605"
      },
      "dob": {
        "date": "1954-07-09T14:01:43.714Z",
        "age": 71
      },
      "registered": {
        "date": "2022-02-02T03:34:03.950Z",
        "age": 3
      },
      "phone": "017687 96364",
      "cell": "07149 397609",
      "id": {
        "name": "NINO",
        "value": "LP 72 77 54 S"
      },
      "picture": {
        "large": "https://randomuser.me/api/portraits/women/8.jpg",
        "medium": "https://randomuser.me/api/portraits/med/women/8.jpg",
        "thumbnail": "https://randomuser.me/api/portraits/thumb/women/8.jpg"
      },
      "nat": "GB"
    }
  ],
  "info": {
    "seed": "42e207ecbc7085df",
    "results": 1,
    "page": 1,
    "version": "1.4"
  }
}

๐Ÿ‘‰๐Ÿป ์ฝ”๋“œ / Code

โœ”๏ธ ContentView.swift


import SwiftUI

struct ContentView: View {
    
    // SwiftUI๋Š” ์ด ViewModel ์•ˆ์˜ @Published ํ”„๋กœํผํ‹ฐ(user, errorMessage)๊ฐ€ ๋ฐ”๋€Œ๋ฉด
    // ์ž๋™์œผ๋กœ ๋ทฐ๋ฅผ ๋‹ค์‹œ ๊ทธ๋ฆฌ๋„๋ก ํ•จ
    // SwiftUI automatically redraws the view when the @Published properties (user, errorMessage) in this ViewModel change.
    @StateObject private var viewModel = RandomUserViewModel()
    
    var body: some View {
        VStack(spacing: 20) {
            
            // ํ”„๋กœํ•„ / Profile name
            if let user = viewModel.user {
                // ํ”„๋กœํ•„ ๋„ค์ž„ / profile name
                Text("\(user.name.first) \(user.name.last)")
                    .font(.title)
                
                // ํ”„๋กœํ•„ ์ด๋ฏธ์ง€ URL์ด๋ฏธ์ง€ ๋น„๋™๊ธฐ ๋กœ๋”ฉ
                // Profile image URL image asynchronous loading
                AsyncImage(url: URL(string: user.picture.large)) { image in
                    image
                        .resizable()
                        .scaledToFit() // ๋น„์œจ์œ ์ง€ํ•˜๋ฉฐ ๋งž์ถค / Custom while maintaining proportions
                        .frame(width: 180, height: 180)
                        .clipShape(Circle())
                } placeholder: {
                    ProgressView() // ๋กœ๋”ฉ์ค‘์—๋Š” ProgressViewํ‘œ์‹œ / Display ProgressView while loading
                }
            }
            
            // ์—๋Ÿฌ ๋ฉ”์„ธ์ง€ / error message
            if let error = viewModel.errorMessage {
                Text("Error: \(error)")
                    .foregroundColor(.red)
            }
            
            // ๋ฒ„ํŠผ / button
            Button(action: {
                viewModel.fetchRandomUser()
            }) {
                Text("๋žœ๋ค ์œ ์ € ๋ถˆ๋Ÿฌ์˜ค๊ธฐ/Load random users")
                    .padding()
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(12)
            }
        }
        .padding()
    }
}


#Preview {
    ContentView()
}

โœ”๏ธ RandomUserModel.swift

import Foundation

// Codable์„ ์“ฐ๋ฉด JSON โ†” Struct ๋ณ€ํ™˜์ด ์ž๋™
// If you use Codable, JSON โ†” Struct conversion is automatic.
struct RandomUserResponse: Codable {
    let results: [RandomUser] // "results": [
}

struct RandomUser: Codable {
    let name: Name            // "results": [ { "name": {
    let picture: Picture
    
    struct Name: Codable {
        let first: String     // "results": [ { "name": { "first": "Lisa","last": "Fowler"
        let last: String
    }
    
    struct Picture: Codable {
        let large: String
    }
}

โœ”๏ธ RandomUserSwiftApp

import SwiftUI

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

โœ”๏ธ RandomUserViewModel

import Foundation
import Combine // add

class RandomUserViewModel: ObservableObject {
    @Published var user: RandomUser?
    @Published var errorMessage: String?
    
    private var cancellables = Set<AnyCancellable>()
    
    func fetchRandomUser() {
        guard let url = URL(string: "https://randomuser.me/api/") else { return }
        
        /*
         Combine์˜ Publisher ํ๋ฆ„
         URLSession.shared.dataTaskPublisher(for: url)
         ๊ธฐ๋ณธ์ ์œผ๋กœ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค๋ ˆ๋“œ์—์„œ ์‹คํ–‰๋จ
         ๋„คํŠธ์›Œํฌ ์‘๋‹ต, ๋ฐ์ดํ„ฐ ๋””์ฝ”๋”ฉ ๋“ฑ์€ ๋น„๋™๊ธฐ ์ž‘์—…
         ์ด ์ƒํƒœ์—์„œ UI๋ฅผ ๋ฐ”๋กœ ์—…๋ฐ์ดํŠธํ•˜๋ฉด ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธธ ์ˆ˜ ์žˆ์Œ
         
         Combine's Publisher Flow
         URLSession.shared.dataTaskPublisher(for: url)
         By default, runs on a background thread.
         Network responses, data decoding, etc. are asynchronous tasks.
         Updating the UI immediately in this state can cause problems.
         */
        
        URLSession.shared.dataTaskPublisher(for: url)
            //Publisher๊ฐ€ (data: Data, response: URLResponse) ํ˜•ํƒœ์ธ๋ฐ ๊ทธ์ค‘ data๋งŒ ์‚ฌ์šฉํ•˜๊ฒ ๋‹ค๋Š” ๋œป
            //Publisher is in the form of (data: Data, response: URLResponse), which means that only data will be used.
            .map { $0.data }
            // JSON โ†’ Swift Struct ๋ณ€ํ™˜.
            // Convert JSON โ†’ Swift Struct.
            .decode(type: RandomUserResponse.self, decoder: JSONDecoder())
            // UI ์—…๋ฐ์ดํŠธ๋Š” ๋ฐ˜๋“œ์‹œ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์—์„œ ํ•ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ด ์‹œ์ (sink) ์ดํ›„ ๋ชจ๋“  downstream ์ž‘์—…์„ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์—์„œ ์‹คํ–‰.(๋ฐฑ๊ทธ๋ž„์šด๋“œ ์Šค๋ ˆ๋“œ์—์„œ ์‹คํ–‰์‹œ ํฌ๋ž˜์‹œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Œ)
            // Since UI updates must be done on the main thread, all downstream work after this point (sink) is executed on the main thread. (Crash may occur if executed on a background thread.)
            .receive(on: DispatchQueue.main)
            .sink(receiveCompletion: { completion in // ๊ตฌ๋…์‹œ์ž‘,์—๋Ÿฌ์ฒ˜๋ฆฌ / Subscription start, error handling
                switch completion {
                case .finished:
                    break
                case .failure(let error):
                    self.errorMessage = error.localizedDescription
                }
            }, receiveValue: { response in
                // <-- ์—ฌ๊ธฐ์„œ ์‹ค์ œ @Published user ํ”„๋กœํผํ‹ฐ๊ฐ€ ๋ฐ”๋€œ(UI๊ฐฑ์‹  ์‹œํ‚ค๋Š” ์œ„์น˜)
                // <-- Here, the actual @Published user property changes (where the UI is updated)
                self.user = response.results.first
            })
            // ๊ตฌ๋… ์œ ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ์ €์žฅ , inoutํ‚ค์›Œ๋“œ in์€ ํŒŒ๋ผ๋ฉ”ํ„ฐ์ด๋ฆ„(๋ ˆ์ด๋ธ”),.storeํ•จ์ˆ˜๊ฐ€ inoutํ‚ค์›Œ๋“œ๋ฅผ ์‚ฌ์šฉํ•จ
            // To maintain subscription, store, inout keyword in is parameter name (label), .store function uses inout keyword
            .store(in: &cancellables)
    }
}

๐Ÿ‘‰๐Ÿป ์Šคํฌ๋ฆฐ์ƒท/ScreenShot

Leave a Reply