[Swift]GET,POST,PATCH,PUT(MacOS)

โญ๏ธ ReactExample7 – server + swift

๐Ÿ‘‰๐Ÿป ์•„๋ž˜๋Š” GET,POST,PATCH,PUT์„ Swift์—์„œ ๊ตฌํ˜„ํ•œ ์˜ˆ์ œ์ž…๋‹ˆ๋‹ค.
Below is an example of implementing GET, POST, PATCH, and PUT in Swift.

๐Ÿ‘‰๐Ÿป ๋ฆฌ์•กํŠธ๋กœ ๊ตฌํ˜„ํ•œ ์˜ˆ์ œ๋Š” ์•„๋ž˜์— ๋งํฌ๋œ ํฌ์ŠคํŠธ๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”
For an example implemented with React, please refer to the post linked below.

๐Ÿ‘‰๐Ÿป Nodejs Express Server(Backend)

โœ”๏ธServer.js

const express = require("express");
const app = express();
const PORT = 3000;

app.use(express.json());

// ๊ฐ€์ƒ์˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค / Virtual Database
let users = [{ id: "1", name: "V", age: 20, nickname: "Cyberpunk2077" }];

// GET: ํŠน์ • ์œ ์ € ์กฐํšŒ / Specific user inquiry
app.get("/users/:id", (req, res) => {
  const user = users.find((u) => u.id === req.params.id);
  user
    ? res.json(user)
    : res.status(404).json({ message: "์œ ์ € ์—†์Œ/No user" });
});

// POST: ์ƒˆ๋กœ์šด ์œ ์ € ์ƒ์„ฑ / Create new user
app.post("/users", (req, res) => {
  const newUser = { id: String(users.length + 1), ...req.body };
  users.push(newUser);
  res.status(201).json(newUser);
});

// PATCH: ๋ฐ์ดํ„ฐ์˜ '์ผ๋ถ€'๋งŒ ์ˆ˜์ • (๊ธฐ์กด ๋ฐ์ดํ„ฐ ์œ ์ง€)
// Modify only 'part' of the data (keep existing data)
app.patch("/users/:id", (req, res) => {
  const index = users.findIndex((u) => u.id === req.params.id);
  if (index !== -1) {
    // ๊ธฐ์กด ๊ฐ’ + ๋ฐ”๋€ ๊ฐ’ ํ•ฉ์น˜๊ธฐ / Merge existing value + changed value
    users[index] = { ...users[index], ...req.body };
    res.json(users[index]);
  } else {
    res.status(404).json({ message: "์œ ์ € ์—†์Œ/No user" });
  }
});

// PUT: ๋ฐ์ดํ„ฐ '์ „์ฒด'๋ฅผ ๊ต์ฒด (๋ณด๋‚ด์ง€ ์•Š์€ ํ•„๋“œ๋Š” ์‚ฌ๋ผ์ง)
// Replace 'all' data (unsent fields disappear)
app.put("/users/:id", (req, res) => {
  const index = users.findIndex((u) => u.id === req.params.id);
  if (index !== -1) {
    // ๊ธฐ์กด ๋‚ด์šฉ ๋ฌด์‹œํ•˜๊ณ  ๋ฎ์–ด์“ฐ๊ธฐ / Ignore existing content and overwrite
    users[index] = { id: req.params.id, ...req.body };
    res.json(users[index]);
  } else {
    res.status(404).json({ message: "์œ ์ € ์—†์Œ/No user" });
  }
});

// DELETE: ๋ฐ์ดํ„ฐ ์‚ญ์ œ / data deletion
app.delete("/users/:id", (req, res) => {
  users = users.filter((u) => u.id !== req.params.id);
  res.json({ message: `User ${req.params.id} ์‚ญ์ œ ์™„๋ฃŒ / Deletion complete` });
});

app.listen(PORT, () => console.log(`Server: http://localhost:${PORT}`));

๐Ÿ‘‰๐Ÿป Swift(Frontend)

โœ”๏ธ ContentView


import SwiftUI

// ๋ฐ์ดํ„ฐ ๋ชจ๋ธ ์ •์˜ / Data model definition
struct User: Codable, Identifiable {
    let id: Int?
    var name: String?
    var age: Int?
    var nickname: String?
}

struct MessageResponse: Codable {
    let message: String
}

struct ContentView: View {
    @State private var userId: String = "1"
        @State private var userData: User? = nil
        @State private var log: String = ""
        
        // ์„œ๋ฒ„ ๊ธฐ๋ณธ URL / Server Base URL
        let baseURL = "http://localhost:3000/users"

        var body: some View {
            ScrollView {
                VStack(alignment: .leading, spacing: 20) {
                    Text("HTTP Methods Playground")
                        .font(.title).bold()

                    TextField("User ID", text: $userId)
                        .textFieldStyle(RoundedBorderTextFieldStyle())
                        .keyboardType(.numberPad)

                    // ๋ฒ„ํŠผ ๊ทธ๋ฃน / Button Group
                    VStack(spacing: 10) {
                        HStack {
                            methodButton("GET (์กฐํšŒ/Search)", color: Color(.systemGray5)) { getUser() }
                            methodButton("POST (์ƒ์„ฑ/Creation)", color: Color(red: 0.8, green: 1, blue: 0.8)) { createUser() }
                        }
                        HStack {
                            methodButton("PATCH (์ผ๋ถ€์ˆ˜์ •/modifications)", color: Color(red: 1, green: 0.95, blue: 0.8)) { patchUser() }
                            methodButton("PUT (์ „์ฒด๊ต์ฒด/replacement)", color: Color(red: 1, green: 0.8, blue: 0.8)) { putUser() }
                        }
                        methodButton("DELETE (์‚ญ์ œ/Deletion)", color: Color(.systemGray6)) { deleteUser() }
                    }

                    HStack(alignment: .top, spacing: 20) {
                        
                        // ํ˜„์žฌ ๋ฐ์ดํ„ฐ ์ƒ์„ธ / Current Data State
                        VStack(alignment: .leading) {
                            Text("ํ˜„์žฌ ๋ฐ์ดํ„ฐ ์ƒ์„ธ / Current data details").font(.headline)
                            if let user = userData {
                                VStack(alignment: .leading) {
                                    Text("ID: \(user.id ?? 0)")
                                    Text("์ด๋ฆ„/Name: \(user.name ?? "์—†์Œ/none")")
                                    Text("๋‚˜์ด/Age: \(user.age != nil ? "\(user.age!)" : "์—†์Œ/none")")
                                    Text("๋‹‰๋„ค์ž„/Nickname: \(user.nickname ?? "์—†์Œ/none")")
                                }
                                .padding()
                                .border(Color.blue)
                            } else {
                                Text("์กฐํšŒ๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค./No data was retrieved.")
                            }
                        }
                        .frame(maxWidth: .infinity, alignment: .leading)

                        // ์„œ๋ฒ„ ์›๋ณธ ์‘๋‹ต / Server original response(Raw JSON)
                        VStack(alignment: .leading) {
                            Text("์„œ๋ฒ„ ์‘๋‹ต / server response").font(.headline)
                            Text(log)
                                .font(.system(.caption, design: .monospaced))
                                .padding()
                                .frame(maxWidth: .infinity, alignment: .leading)
                                .background(Color(.systemGray6))
                        }
                        .frame(maxWidth: .infinity, alignment: .leading)
                    }
                }
                .padding()
            }
}

    // MARK: - HTTP Methods
    func getUser() {
       performRequest(url: "\(baseURL)/\(userId)", method: "GET")
    }

    func createUser() {
       let body: [String: Any] = ["name": "NewUser", "age": 30, "nickname": "SilverHand"]
       performRequest(url: baseURL, method: "POST", body: body)
    }

    func patchUser() {
       let body: [String: Any] = ["nickname": "Johnny"]
       performRequest(url: "\(baseURL)/\(userId)", method: "PATCH", body: body)
    }

    func putUser() {
       // PUT์€ ๋ณดํ†ต ๋ฆฌ์†Œ์Šค ์ „์ฒด๋ฅผ ๊ฐˆ์•„๋ผ์šฐ๋ฏ€๋กœ nickname์„ ์ƒ๋žตํ•˜๋ฉด null์ด ๋  ๊ฐ€๋Šฅ์„ฑ์„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ํ•ฉ๋‹ˆ๋‹ค.
       // Since PUT usually replaces the entire resource, omitting nickname simulates the possibility that it will be null.
       let body: [String: Any] = ["name": "John", "age": 25]
       performRequest(url: "\(baseURL)/\(userId)", method: "PUT", body: body)
    }

    func deleteUser() {
        Task {
            // Task๋ฅผ ํ†ตํ•ด ๋ฉ”์ธ ์•กํ„ฐ์™€ ๋น„๋™๊ธฐ ์ปจํ…์ŠคํŠธ๋ฅผ ์—ฐ๊ฒฐ
            // Connect the main actor and the asynchronous context through Task

            guard let url = URL(string: "\(baseURL)/\(userId)") else { return }
            var request = URLRequest(url: url)
            request.httpMethod = "DELETE"
            
            do {
                let (data, _) = try await URLSession.shared.data(for: request)
                let decoded = try JSONDecoder().decode(MessageResponse.self, from: data)
                
                // UI ์—…๋ฐ์ดํŠธ๋Š” ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์—์„œ
                // UI updates are done on the main thread
                await MainActor.run {
                    self.userData = nil
                    self.log = decoded.message
                }
            } catch {
                print("Error: \(error)")
            }
        }
    }

    // MARK: - Helper Methods
    
    // ์„œ๋ฒ„์— ์š”์ฒญ / Request to server
    func performRequest(url urlString: String, method: String, body: [String: Any]? = nil) {
        guard let url = URL(string: urlString) else { return }
        var request = URLRequest(url: url)
        request.httpMethod = method
            
        if let body = body {
          request.addValue("application/json", forHTTPHeaderField: "Content-Type")
          request.httpBody = try? JSONSerialization.data(withJSONObject: body)
        }

        URLSession.shared.dataTask(with: request) { data, response, error in
            guard let data = data else { return }
                    
            // UI ์—…๋ฐ์ดํŠธ๋Š” ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์—์„œ
            // UI updates are done on the main thread
            DispatchQueue.main.async {
              self.log = String(data: data, encoding: .utf8) ?? ""
              if let decodedUser = try? JSONDecoder().decode(User.self, from: data) {
                 self.userData = decodedUser
              } else {
                 self.userData = nil
              }
            }
        }.resume()
    }

    func methodButton(_ title: String, color: Color, action: @escaping () -> Void) -> some View {
      Button(action: action) {
        Text(title)
            .font(.caption)
            .padding(10)
            .frame(maxWidth: .infinity)
            .background(color)
            .foregroundColor(.black)
            .cornerRadius(5)
      }
    }
}

#Preview {
    ContentView()
}

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

Leave a Reply