๐๐ป ์๋๋ 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