I am trying to implement a simple 3-screen application on SwiftUI. The 1st screen allows the user to select a photo, the 2nd must show the loading screen while the photo is analyzed, and the 3rd shows analysis results. Currently, my application doesn’t show the loading screen at all and just jumps to the 3rd screen.
ContentView.swift:
struct ContentView: View {
@State var image: Image = Image("MyImageName")
@State var tiles: [Tile] = []
@State var isSelectionView: Bool = true
@State var isLoadingView: Bool = false
@State var isAdjustmentView: Bool = false
var body: some View {
if (isSelectionView) {
SelectionView(image: $image, isSelectionView: $isSelectionView, isLoadingView: $isLoadingView)
}
if (isLoadingView) {
LoadingMLView(image: $image, tiles: $tiles, isLoadingView: $isLoadingView, isAdjustmentView: $isAdjustmentView)
}
if (isAdjustmentView) {
AdjustmentView(tiles: $tiles)
}
}}
SelectionView.swift:
struct SelectionView: View {
@State var showCaptureImageView = false
@State var showPopover = false
@State var sourceType: UIImagePickerController.SourceType = .camera
@State var imageSelected = false
@Binding var image: Image
@Binding var isSelectionView: Bool
@Binding var isLoadingView: Bool
var body: some View {
VStack() {
if (!showCaptureImageView && !imageSelected) {
Spacer()
Button(action: {
showPopover.toggle()
}) {
Text("Choose photo")
.frame(width: 100, height: 100)
.foregroundColor(Color.white)
.background(Color.blue)
.clipShape(Circle())
}
.confirmationDialog("Select location", isPresented: $showPopover) {
Button("Camera") {
sourceType = .camera
showPopover.toggle()
showCaptureImageView.toggle()
}
Button("Photos") {
sourceType = .photoLibrary
showPopover.toggle()
showCaptureImageView.toggle()
}
}
} else if (showCaptureImageView) {
CaptureImageView(isShown: $showCaptureImageView, image: $image, sourceType: $sourceType)
} else {
ActivityIndicator()
.frame(width: 200, height: 200)
.foregroundColor(.blue)
}
}
.onChange(of: image) { image in
showCaptureImageView.toggle()
imageSelected.toggle()
isSelectionView.toggle()
isLoadingView.toggle()
}
}}
LoadingMLView.swift:
struct LoadingMLView: View {
@Binding var image: Image
@Binding var tiles: [Tile]
@Binding var isLoadingView: Bool
@Binding var isAdjustmentView: Bool
var body: some View {
ActivityIndicator()
.frame(width: 200, height: 200)
.foregroundColor(.blue)
.onAppear {
do {
let model = try VNCoreMLModel(for: MyModel().model)
let request = VNCoreMLRequest(model: model, completionHandler: myResultsMethod)
let uiImage: UIImage = image.asUIImage()
guard let ciImage = CIImage(image: uiImage) else {
fatalError("Unable to create (CIImage.self) from (image).")
}
let handler = VNImageRequestHandler(ciImage: ciImage)
do {
try handler.perform([request])
} catch {
print("Something went wrong!")
}
func myResultsMethod(request: VNRequest, error: Error?) {
guard let results = request.results as? [VNRecognizedObjectObservation]
else { fatalError("error") }
for result in results {
let tile = Tile(name: result.labels[0].identifier)
tiles.append(tile)
}
isLoadingView.toggle()
isAdjustmentView.toggle()
}
} catch {
print("Error:", error)
}
}
}}
No matter how I change the toggle between screens, it just doesn’t seem reactive and the views don’t switch momentarily. What am I doing wrong? Is there a proper way to show loading screen right after the image is selected?
2
Answers
Turns out, the main performance issue was caused by the image type conversion:
After passing the image as UIImage in the first place, the app started working fast, so there was no need to handle the loading in the first place.
Seems like you’re missing an
else
to avoid rendering the screens you don’t want:But, in general what you want is also called "state machine" – essentially your app can be in one of 3 possible states: selection, loading, adjustment, so you should have an enum
@State
to represent the current state, instead of the 3 bool values:When you want to switch to the next screen, just change
state
value…