기능구현 #2
이미지 표시하기
이미지 표시하기
| ContentView > Button
struct ContentView: View {
@ObservedObject var viewModel = ViewModel()
@State var image: UIImage?
@State var text = ""
var body: some View {
NavigationView {
VStack {
Spacer()
앞서 초기화 한 ViewModel을 사용하기 위해 ObsevedObject로 viewMocdel 인스턴스를 생성한다.
var body: some View {
NavigationView {
VStack {
Spacer()
if let image = image {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 300, height: 300)
} else {
Text("Type prompt to generatre image!")
}
Spacer()
TextField("prompt here...", text: $text)
.padding()
Button {
} label: {
Text("Generate")
}
}
.padding()
.navigationTitle("Image Generator")
.onAppear {
viewModel.setup()
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
NavigationLink {
InfoView()
} label: {
Image(systemName: "info.circle")
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button {
//share
} label: {
Image(systemName: "square.and.arrow.up")
}
}
}
}
}
ContentView가 초기화되는 시점에 ViewModel도 함께 초기화될 수 있도록
onAppear modifier를 통해 setup 메서드를 호출한다.
이제부터는 ContentView에서 해당 viewModel 인스턴스 내의 속성과 메서드에 접근할 수 있다.
VStack {
Spacer()
if let image = image {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 300, height: 300)
} else {
Text("Type prompt to generatre image!")
}
Spacer()
TextField("prompt here...", text: $text)
.padding()
Button {
if text.trimmingCharacters(in: .whitespaces) {
}
} label: {
Text("Generate")
}
}
화면 하단의 'Generate' 버튼을 누르면 이미지를 생성하게 된다.
우선 입력된 키워드를 검증하는 것으로 이미지 생성 작업을 시작하게 되는데
Button {
if text.trimmingCharacters(in: .whitespaces) {
}
} label: {
Text("Generate")
}
입력된 문자열의 시작과 끝의 의미 없는 공백을 삭제할 수 있도록 trimmingCharacters를 사용한다.
단, if의 조건으로 전달되는 반환 값은 Boolean 이어야 하지만 trimmingCharacters의 반환값은 새로운 '문자열'이라는 문제가 있다.
Button {
if text.trimmingCharacters(in: .whitespaces).isEmpty {
}
} label: {
Text("Generate")
}
이렇게 isEmpty 를 사용해 trimmingCharacters의 반환값이 존재한다면 False를,
존재하지 않는다면 True를 반환하도록 조치하고, 지금은 조건이 의도와는 반대로 동작하기 때문에
Button {
if !text.trimmingCharacters(in: .whitespaces).isEmpty {
}
} label: {
Text("Generate")
}
이렇게 not을 붙여 결과적으로 trimmingCharacters의 반환값이 존재한다면 True를,
존재하지 않는다면 False를 반환하도록 구현한다.
Button {
if !text.trimmingCharacters(in: .whitespaces).isEmpty {
Task {
let result = await viewModel.generateImage(prompt: text)
}
}
} label: {
Text("Generate")
}
조건이 만족했으므로 이제 키워드를 사용해 이미지를 생성하면 된다.
호출하려는 generateImage 메서드는 이전에 'async-await'를 사용해 비동기 방식으로 구현했기 때문에,
호출할 때는 'Task-await' 방식으로 호출해야 한다.
Button {
if !text.trimmingCharacters(in: .whitespaces).isEmpty {
Task {
let result = await viewModel.generateImage(prompt: text)
if result == nil {
print("Failed to get image")
}
self.image = result
}
}
} label: {
Text("Generate")
}
결과가 정상적으로 반환 됐다면 image 속성에 해당 결과를 저장하고,
문제가 발생했다면 콘솔에 로그를 출력하도록 구현했다.
주어진 키워드에 따라 이미지를 제대로 생성하는 것을 확인할 수 있다.
이미지 표시하기
| Indicator 디자인하기
문제는 비동기로 처리되는 탓에 현재 어떤 상황인지 사용자는 확인할 수가 없다.
때문에 버튼을 여러번 눌러 중복으로 이미지를 생성할 수도 있다.
이를 위한 인디케이터와 작업 제한을 구현해 보자.
struct ContentView: View {
@ObservedObject var viewModel = ViewModel()
@State var processState = false
@State var image: UIImage?
@State var text = ""
var body: some View {
NavigationView {
VStack {
Spacer()
상태를 표시하기 위해 State 변수를 하나 선언한다.
processState의 상태에 따라 인디케이터를 표시하고, 버튼을 비활성화하는 trigger로 사용한다.
NavigationView {
.
.
.
}
.overlay {
if processState {
}
}
화면의 최상단에, 그리고 중앙에 표시될 수 있도록 ContentView의 NavigationView에 overlay를 추가한다.
if문의 조건으로 processState를 전달해 값에 따라 표시될 수 있도록 구성한다.
NavigationView {
.
.
.
}
.overlay {
if processState {
ZStack(alignment: .center) {
Color(uiColor: .black)
.opacity(0.3)
.ignoresSafeArea()
}
}
}
작업 중임을 알리기 위해 화면을 어둡게 하는 Dimming 효과를 추가한다.
해당 과정으로 모든 UI가 overlay 아래로 숨게 되므로 다른 요소의 작동을 막는 기능도 동시에 하게 된다.
NavigationView {
.
.
.
}
.overlay {
if processState {
ZStack(alignment: .center) {
Color(uiColor: .black)
.opacity(0.3)
.ignoresSafeArea()
Rectangle()
.background(Color(uiColor: .lightGray))
.opacity(0.3)
.frame(width: 100, height: 100)
.cornerRadius(15)
}
}
}
인디케이터의 배경이 될 View를 하나 생성한다.
일반적으로 생각하는 blur에 해당하는 material을 사용할 수도 있겠지만,
해당 방식은 iOS15 이상에서 사용되는 방식으로 범용성이 나은 다른 방식을 사용했다.
- background
대비를 위해 유색의 배경색을 지정했다.
lightgray를 사용했다. - opacity
비치는 효과를 위해 투명도를 지정했다.
0.3을 사용했다. - frame
적당한 크기로 표시되도록 frame을 지정했다. - cornerRadius
둥근 모서리를 위해 사용했다.
값이 너무 커지면 원으로 표시되니 주의하자.
물론 기존에 만들어진 괜찮은 패키지를 사용하는 방법도 있다.
관련 내용은 위 글의 내용을 참고하자.
NavigationView {
.
.
.
}
.overlay {
if processState {
ZStack(alignment: .center) {
Color(uiColor: .black)
.opacity(0.3)
.ignoresSafeArea()
Rectangle()
.background(Color(uiColor: .lightGray))
.opacity(0.3)
.frame(width: 100, height: 100)
.cornerRadius(15)
ProgressView()
.tint(.black)
}
}
}
배경 위에 작업이 진행 중임을 알리는 ProgressView를 표시한다.
대비를 위해 검은색으로 색상을 변경했다.
이미지 표시하기
| Indicator 동작 구현하기
Button {
if !text.trimmingCharacters(in: .whitespaces).isEmpty {
Task {
processState.toggle()
let result = await viewModel.generateImage(prompt: text)
if result == nil {
print("Failed to get image")
}
self.image = result
processState.toggle()
}
}
} label: {
Text("Generate")
}
모든 것이 준비 됐으므로 간단하게 Task가 시작하면 processState를 true로 변경하고,
Task가 종료되면 flase로 변경하면 된다.
이미지 생성이 시작되면 화면이 어두워지고, ProgressView가 표시된다.
작업이 끝나면 다시 사라진다.
이미지 표시하기
| 일부 요소 비활성화 하기
물론 화면 전체를 막는 대신 일부 요소만 비활성화 하는 것도 가능하다.
NavigationView {
.
.
.
}
.overlay {
if processState {
ZStack(alignment: .center) {
Rectangle()
.background(Color(uiColor: .lightGray))
.opacity(0.3)
.frame(width: 100, height: 100)
.cornerRadius(15)
ProgressView()
.tint(.black)
}
}
}
스크린 전체를 가릴 것이 아니므로 Dimming 역할을 했던 View는 삭제한다.
Button {
if !text.trimmingCharacters(in: .whitespaces).isEmpty {
Task {
processState.toggle()
let result = await viewModel.generateImage(prompt: text)
if result == nil {
print("Failed to get image")
}
self.image = result
processState.toggle()
}
}
} label: {
Text("Generate")
}
.disabled(processState)
이후 processState에 비활성화 될 수 있도록 View에 disabled modifier를 추가하고, 파라미터로 플래그를 전달한다.
화면이 Dimming 되고, 전체가 비활성화 되는 것과는 달리 Generate 버튼만 비활성화된다.
'프로젝트 > Image Generator (w∕OpenAI)' 카테고리의 다른 글
05. 인터페이스 디자인 #2 (0) | 2022.12.28 |
---|---|
04. 기능구현 #3 (0) | 2022.12.28 |
02. 기능 구현 #1 (0) | 2022.12.27 |
01. 인터페이스 디자인 #1 (0) | 2022.12.24 |
00. 시작하며 (0) | 2022.12.23 |