๐ค Swift์ SwiftUI๋ก UI ์ฝ๋ ์๋ ์์ฑ ์ฑ ๋ง๋ค๊ธฐ
๐ก์ด ๊ธ์ corca์ LLMํ๋ก๋ํธ dev.post ๊ฐ ์์ฑํ ๊ธ์ ๋๋ค๐ค ## ViewModel์ ๋ง๋ UI ์ฝ๋๋ฅผ OpenAI API๋ก ์๋ ์์ฑํ๊ธฐ ์ฌ๋ฌ๋ถ, SwiftUI๋ฅผ ์ฌ์ฉํ๋ฉด์ UI ์ฝ๋๋ฅผ ์์ฑํ๋ ๊ณผ์ ์์ ๋ฒ๊ฑฐ๋ก์์ ๋๋ ์ ์์ผ์ ๊ฐ์? ๋ง์ฝ, ViewModel์ ๋ฐ๋ผ ์๋์ผ๋ก UI๋ฅผ ์์ฑํด์ฃผ๋ ์ฑ์ด ์๋ค๋ฉด ์ผ๋ง๋ ํธ๋ฆฌํ ๊น์? ์ค๋์ ๊ทธ๋ฐ ๋๋ผ์ด ์ฑ์ ๋ง๋๋ ๊ณผ์ ์ ํจ๊ป ์์๋ณด๊ฒ ์ต๋๋ค! ์ด ํ๋ก์ ํธ์ ๋ชฉํ๋ ์ด๋ฏธ์ง๋ฅผ ์ ๋ ฅํ์ ๋, ๊ทธ์ ๋ง๋ UI ์ฝ๋๋ฅผ ์๋์ผ๋ก ์์ฑํด์ฃผ๋ ์ฑ์ ๋๋ค. ์ด๋ฅผ ์ํด OpenAI API๋ฅผ ํ์ฉํ์ฌ ์ด๋ฏธ์ง๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์ป์ ๋ฐ์ดํฐ๋ฅผ ๋ฐํ์ผ๋ก SwiftUI ์ฝ๋๋ฅผ ์์ฑํฉ๋๋ค. ํ๋ก์ ํธ์ ์ ์ฒด์ ์ธ ๊ตฌ์กฐ์ ์ค์ํ ์ฝ๋ ๋ถ๋ถ์ ํจ๊ป ์ดํด๋ณด๊ฒ ์ต๋๋ค. ### ViewModel์ ๋ง๊ฒ UI ์ง๊ธฐ SwiftUI์์ ViewModel์ ์ฌ์ฉํ๋ฉด ๋ฐ์ดํฐ์ UI ๋ก์ง์ ๋ถ๋ฆฌํ์ฌ ๋์ฑ ๊น๋ํ ์ฝ๋๋ฅผ ์์ฑํ ์ ์์ต๋๋ค. ํ๋ก์ ํธ์๋ ImageLoadViewModel ์ด๋ผ๋ ViewModel์ด ์์ต๋๋ค. ์ด ViewModel์ ์ด๋ฏธ์ง๋ฅผ ๋ถ์ํ๊ณ , ๋ถ์ ๊ฒฐ๊ณผ๋ฅผ UI์ ๋ฐ์ํ๋ ์ญํ ์ ํฉ๋๋ค. #### ImageLoadViewModel ์ฝ๋ ์ค๋ช ๋จผ์ , ImageLoadViewModel ์ ์ค์ํ ๋ถ๋ถ์ ์ดํด๋ณด๊ฒ ์ต๋๋ค: ```swift class ImageLoadViewModel: ObservableObject { enum ViewState { case load, loaded, analysis, content, completed, error } @Published var message: String = "" @Published var viewState: ViewState = .load private var apiController: APIController private var cancellables = Set() init(apiController: APIController = APIController()) { self.apiController = apiController } func analyze(base64Image: String, userContent: String) { viewState = .analysis apiController.makeRequest(base64Image: base64Image, userContent: userContent) .sink { completion in switch completion { case .failure(let error): print(error) self.viewState = .error case .finished: break } } receiveValue: { response in self.message = response.message self.viewState = .completed } .store(in: &cancellables) } } ``` #### ์ฃผ์ ๊ตฌ์ฑ ์์ ์ค๋ช - **`ViewState` ์ด๊ฑฐํ**: ๊ฐ๊ธฐ ๋ค๋ฅธ UI ์ํ๋ฅผ ํํํฉ๋๋ค. (`load`, loaded , analysis , content , completed , error ) - **`@Published var message`**: ๋ถ์ ๊ฒฐ๊ณผ ๋ฉ์์ง๋ฅผ ์ ์ฅํฉ๋๋ค. - **`@Published var viewState`**: ํ์ฌ ์ํ๋ฅผ ์ ์ฅํ๊ณ , ์ํ๊ฐ ๋ฐ๋ ๋๋ง๋ค UI๊ฐ ์ ๋ฐ์ดํธ๋ฉ๋๋ค. - **`apiController`**: OpenAI API์ ํต์ ํ๋ ์ปจํธ๋กค๋ฌ. - **`analyze` ํจ์**: ์ด๋ฏธ์ง๋ฅผ ๋ถ์ํ๊ณ ๊ฒฐ๊ณผ๋ฅผ ์ฒ๋ฆฌํฉ๋๋ค. ### OpenAI API์์ ํต์ ์ด์ OpenAI API์ ํต์ ํ๋ ๋ถ๋ถ์ ์ดํด๋ณด๊ฒ ์ต๋๋ค. ์ด ๋ถ๋ถ์์๋ Alamofire๋ฅผ ์ฌ์ฉํ์ฌ ๋คํธ์ํฌ ์์ฒญ์ ์ฒ๋ฆฌํฉ๋๋ค. ```swift class APIController { let baseURL = "https://api.openai.com/v1/whatever" func makeRequest(base64Image: String, userContent: String) -> AnyPublisher { let payload = Payload(model: "gpt-3.5-turbo", messages: [Message(content: userContent, imageURL: base64Image)]) let headers: HTTPHeaders = [ "Content-Type": "application/json", "Authorization": "Bearer YOUR_OPENAI_API_KEY" ] return AF.request(baseURL, method: .post, parameters: payload, encoder: JSONParameterEncoder.default, headers: headers) .validate() .publishDecodable(type: Response.self) .tryCompactMap { $0.value } .eraseToAnyPublisher() } } ``` #### ์ฃผ์ ๊ตฌ์ฑ ์์ ์ค๋ช - **`baseURL`**: OpenAI API ์๋ํฌ์ธํธ URL. - **`makeRequest` ํจ์**: API ์์ฒญ์ ์์ฑํ๊ณ ๊ฒฐ๊ณผ๋ฅผ AnyPublisher ํ์์ผ๋ก ๋ฐํํฉ๋๋ค. - **`Alamofire`**: HTTP ๋คํธ์ํฌ ์์ฒญ์ ๊ด๋ฆฌํ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก, AF ๋ผ๋ ์ค์๋ง๋ก ์ฌ์ฉ๋ฉ๋๋ค. ### ImageLoadView UI ๊ตฌํํ๊ธฐ ์ด์ ViewModel์ ํ์ฉํ์ฌ ์ค์ UI๋ฅผ ๊ตฌ์ฑํ๋ ๋ฐฉ๋ฒ์ ์ดํด๋ณด๊ฒ ์ต๋๋ค. ImageLoadView ๋ ์ฌ๋ฌ ์ํ์ ๋ฐ๋ผ ๋ค๋ฅธ UI๋ฅผ ๋ณด์ฌ์ค๋๋ค. ```swift struct ImageLoadView: View { @State private var userInput: String = "" @State private var selectedImage: Image? @State private var selectedItem: PhotosPickerItem? @StateObject private var viewModel = ImageLoadViewModel() var body: some View { VStack { if viewModel.viewState == .load { PhotosPicker("์ด๋ฏธ์ง ์ ํ", selection: $selectedItem) .onChange(of: selectedItem) { newItem in Task { if let data = try? await newItem?.loadTransferable(type: Data.self), let uiImage = UIImage(data: data) { selectedImage = Image(uiImage: uiImage) viewModel.viewState = .loaded } } } .padding() } else if viewModel.viewState == .loaded { VStack { selectedImage? .resizable() .scaledToFit() .frame(width: 300, height: 300) TextField("์์ฒญํ ๋ด์ฉ์ ์ ๋ ฅํ์ธ์", text: $userInput) .padding() .textFieldStyle(RoundedBorderTextFieldStyle()) Button("๋ถ์ ์์ฒญ") { if let selectedImage = selectedImage { // UIImage to base64 let base64Image = imageToBase64(image: selectedImage) viewModel.analyze(base64Image: base64Image, userContent: userInput) } } .padding() } } else if viewModel.viewState == .analysis { ProgressView("๋ถ์ ์ค...") .progressViewStyle(CircularProgressViewStyle()) .padding() } else if viewModel.viewState == .completed { Text(viewModel.message) .padding() } else if viewModel.viewState == .error { Text("์๋ฌ๊ฐ ๋ฐ์ํ์ต๋๋ค.") .foregroundColor(.red) .padding() } } } private func imageToBase64(image: Image) -> String { // Image๋ฅผ Base64๋ก ๋ณํํ๋ ๋ก์ง } } ``` #### ์ฃผ์ ๊ตฌ์ฑ ์์ ์ค๋ช - **`PhotosPicker`**: ์ด๋ฏธ์ง๋ฅผ ์ ํํ ์ ์๋ ์ํธ์์ฉ ์์. - **`@StateObject`์ @State **: SwiftUI์ ์ํ ๊ด๋ฆฌ๋ฅผ ์ํ property wrappers. - **`VStack`, TextField , Button **: ๋ค์ํ UI ์ปดํฌ๋ํธ๋ค์ ๋ฐฐ์นํ๋ ๋ฐ ์ฌ์ฉ๋ฉ๋๋ค. - **์ํ์ ๋ฐ๋ฅธ UI ๋ณํ**: viewState ์ ๋ฐ๋ผ ๋ค๋ฅธ UI๊ฐ ๋ ๋๋ง๋ฉ๋๋ค. ### ๊ฒฐ๋ก ์ค๋์ ViewModel์ ๊ธฐ๋ฐ์ผ๋ก UI ์ฝ๋๋ฅผ ์๋์ผ๋ก ์์ฑํด์ฃผ๋ ์ฑ์ ๋ง๋๋ ๊ณผ์ ์ ์ดํด๋ดค์ต๋๋ค. OpenAI API๋ฅผ ํ์ฉํ์ฌ ์ด๋ฏธ์ง๋ฅผ ๋ถ์ํ๊ณ , ๊ทธ ๊ฒฐ๊ณผ๋ฅผ UI์ ๋ฐ์ํ๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์ ์ต๋๋ค. ์ด๋ ๊ฒ ํ๋ฉด ๋ณด๋ค ํจ์จ์ ์ผ๋ก UI ์ฝ๋๋ฅผ ์์ฑํ ์ ์์ผ๋ฉฐ, ์ฝ๋๋ฅผ ๋์ฑ ๊น๋ํ๊ฒ ์ ์งํ ์ ์์ต๋๋ค. SwiftUI์ OpenAI API๋ฅผ ํจ๊ป ํ์ฉํด ๋ณด์ธ์! ์ด ๊ธ์ด ๋์์ด ๋์๊ธฐ๋ฅผ ๋ฐ๋๋๋ค. ๊ถ๊ธํ ์ ์ด๋ ํผ๋๋ฐฑ์ด ์๋ค๋ฉด ์ธ์ ๋ ์ง ๋๊ธ๋ก ๋จ๊ฒจ์ฃผ์ธ์. ๊ฐ์ฌํฉ๋๋ค! ๐