Community

๐Ÿค– 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๋ฅผ ํ•จ๊ป˜ ํ™œ์šฉํ•ด ๋ณด์„ธ์š”! ์ด ๊ธ€์ด ๋„์›€์ด ๋˜์—ˆ๊ธฐ๋ฅผ ๋ฐ”๋ž๋‹ˆ๋‹ค. ๊ถ๊ธˆํ•œ ์ ์ด๋‚˜ ํ”ผ๋“œ๋ฐฑ์ด ์žˆ๋‹ค๋ฉด ์–ธ์ œ๋“ ์ง€ ๋Œ“๊ธ€๋กœ ๋‚จ๊ฒจ์ฃผ์„ธ์š”. ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค! ๐Ÿ˜Š

์•Œ๋ฆผ

์•Œ๋ฆผ์ด ์—†์Šต๋‹ˆ๋‹ค