Skip to content

Commit 3884409

Browse files
authored
Style Imagen UI (#1676)
* Improve the styling of the view - use the re-usable InputField component - layout the images in a 2x2 grid - add rounded corners - add a progress overlay * Allow user to cancel the image generation process --------- Signed-off-by: Peter Friese <[email protected]>
1 parent de817cb commit 3884409

File tree

2 files changed

+87
-38
lines changed

2 files changed

+87
-38
lines changed

vertexai/ImagenScreen/ImagenScreen.swift

Lines changed: 59 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// limitations under the License.
1414

1515
import SwiftUI
16+
import GenerativeAIUIComponents
1617

1718
struct ImagenScreen: View {
1819
@StateObject var viewModel = ImagenViewModel()
@@ -25,29 +26,38 @@ struct ImagenScreen: View {
2526
var focusedField: FocusedField?
2627

2728
var body: some View {
28-
VStack {
29-
TextField("Enter a prompt to generate an image", text: $viewModel.userInput)
30-
.focused($focusedField, equals: .message)
31-
.textFieldStyle(.roundedBorder)
32-
.onSubmit {
33-
onGenerateTapped()
29+
ZStack {
30+
VStack {
31+
InputField("Enter a prompt to generate an image", text: $viewModel.userInput) {
32+
Image(
33+
systemName: viewModel.inProgress ? "stop.circle.fill" : "paperplane.circle.fill"
34+
)
35+
.font(.title)
3436
}
35-
.padding()
37+
.focused($focusedField, equals: .message)
38+
.onSubmit { sendOrStop() }
3639

37-
Button("Generate") {
38-
onGenerateTapped()
40+
ScrollView {
41+
let spacing: CGFloat = 10
42+
LazyVGrid(columns: [
43+
GridItem(.fixed(UIScreen.main.bounds.width / 2 - spacing), spacing: spacing),
44+
GridItem(.fixed(UIScreen.main.bounds.width / 2 - spacing), spacing: spacing),
45+
], spacing: spacing) {
46+
ForEach(viewModel.images, id: \.self) { image in
47+
Image(uiImage: image)
48+
.resizable()
49+
.aspectRatio(contentMode: .fill)
50+
.frame(width: UIScreen.main.bounds.width / 2 - spacing,
51+
height: UIScreen.main.bounds.width / 2 - spacing)
52+
.cornerRadius(12)
53+
.clipped()
54+
}
55+
}
56+
.padding(.horizontal, spacing)
57+
}
3958
}
40-
.padding()
4159
if viewModel.inProgress {
42-
Text("Waiting for model response ...")
43-
}
44-
ForEach(viewModel.images, id: \.self) {
45-
Image(uiImage: $0)
46-
.resizable()
47-
.scaledToFill()
48-
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
49-
.aspectRatio(nil, contentMode: .fit)
50-
.clipped()
60+
ProgressOverlay()
5161
}
5262
}
5363
.navigationTitle("Imagen sample")
@@ -56,11 +66,38 @@ struct ImagenScreen: View {
5666
}
5767
}
5868

59-
private func onGenerateTapped() {
60-
focusedField = nil
61-
69+
private func sendMessage() {
6270
Task {
6371
await viewModel.generateImage(prompt: viewModel.userInput)
72+
focusedField = .message
73+
}
74+
}
75+
76+
private func sendOrStop() {
77+
if viewModel.inProgress {
78+
viewModel.stop()
79+
} else {
80+
sendMessage()
81+
}
82+
}
83+
}
84+
85+
struct ProgressOverlay: View {
86+
var body: some View {
87+
ZStack {
88+
RoundedRectangle(cornerRadius: 16)
89+
.fill(Material.ultraThinMaterial)
90+
.frame(width: 120, height: 100)
91+
.shadow(radius: 8)
92+
93+
VStack(spacing: 12) {
94+
ProgressView()
95+
.scaleEffect(1.5)
96+
97+
Text("Loading...")
98+
.font(.subheadline)
99+
.foregroundColor(.secondary)
100+
}
64101
}
65102
}
66103
}

vertexai/ImagenScreen/ImagenViewModel.swift

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ class ImagenViewModel: ObservableObject {
3535

3636
private let model: ImagenModel
3737

38+
private var generateImagesTask: Task<Void, Never>?
39+
3840
// 1. Initialize the Vertex AI service
3941
private let vertexAI = VertexAI.vertexAI()
4042

@@ -57,27 +59,37 @@ class ImagenViewModel: ObservableObject {
5759
}
5860

5961
func generateImage(prompt: String) async {
60-
guard !inProgress else {
61-
print("Already generating images...")
62-
return
63-
}
64-
do {
62+
stop()
63+
64+
generateImagesTask = Task {
65+
inProgress = true
6566
defer {
6667
inProgress = false
6768
}
68-
inProgress = true
69-
// 4. Call generateImages with the text prompt
70-
let response = try await model.generateImages(prompt: prompt)
7169

72-
// 5. Print the reason images were filtered out, if any.
73-
if let filteredReason = response.filteredReason {
74-
print("Image(s) Blocked: \(filteredReason)")
75-
}
70+
do {
71+
// 4. Call generateImages with the text prompt
72+
let response = try await model.generateImages(prompt: prompt)
73+
74+
// 5. Print the reason images were filtered out, if any.
75+
if let filteredReason = response.filteredReason {
76+
print("Image(s) Blocked: \(filteredReason)")
77+
}
7678

77-
// 6. Convert the image data to UIImage for display in the UI
78-
images = response.images.compactMap { UIImage(data: $0.data) }
79-
} catch {
80-
logger.error("Error generating images: \(error)")
79+
if !Task.isCancelled {
80+
// 6. Convert the image data to UIImage for display in the UI
81+
images = response.images.compactMap { UIImage(data: $0.data) }
82+
}
83+
} catch {
84+
if !Task.isCancelled {
85+
logger.error("Error generating images: \(error)")
86+
}
87+
}
8188
}
8289
}
90+
91+
func stop() {
92+
generateImagesTask?.cancel()
93+
generateImagesTask = nil
94+
}
8395
}

0 commit comments

Comments
 (0)