How to handle loading screens in SwiftUI? - ios

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?

Seems like you're missing an else to avoid rendering the screens you don't want:
var body: some View {
if (isSelectionView) {
SelectionView(image: $image, isSelectionView: $isSelectionView, isLoadingView: $isLoadingView)
} else if (isLoadingView) {
LoadingMLView(image: $image, tiles: $tiles, isLoadingView: $isLoadingView, isAdjustmentView: $isAdjustmentView)
} else if (isAdjustmentView) {
AdjustmentView(tiles: $tiles)
}
}}
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:
struct ContentView: View {
// ...
enum AppState { case selection, loading, adjustment }
#State private var state = AppState.selection
var body: some View {
switch state {
case .selection:
SelectionView(image: $image, state: $state)
case .loading:
LoadingMLView(image: $image, tiles: $tiles, state: $state)
case .adjustment:
AdjustmentView(tiles: $tiles)
}
}
When you want to switch to the next screen, just change state value...

Turns out, the main performance issue was caused by the image type conversion:
let uiImage: UIImage = image.asUIImage()
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.

Related

ImagePicker showing duplicate photos

I am trying to understand why when I add a photo in my view it is duplicating the photo?
I want the user to add a photo to each card separately. I have tried to directly add selectedImage to my struct item image within the [Expense] array but xcode is screaming at me.
I know it has to with the #State selectedImage with its #Binding but Im not 100% sure how to enable this to work in my current scenario?
Here is the struct it is conforming to:
struct Card: Identifiable {
var id = UUID()
var title: String
var expenses: [Expense]
mutating func addExpenses() {
expenses.append(Expense(expensetype: "", amount: 0.0))
}
}
struct Expense: Identifiable {
var id = UUID()
var expensetype: String
var amount: Double = 0.0
var image: UIImage?
}
I am creating an array of cards on a button press in my contentview {
struct ContentView: View {
#State private var cards = [Card]()
#State private var sourceType: UIImagePickerController.SourceType = .photoLibrary
#State private var selectedImage: UIImage?
#State private var isImagePickerDisplay = false
#State private var showingOptions = false
var title = ""
var expensetype = ""
var amount: Double = 0.0
var image: UIImage?
var body: some View {
NavigationStack {
Form {
List {
Button("Add card") {
addCard()
}
ForEach($cards) { a in
Section {
TextField("Title", text: a.title)
Button("Add expense") {
a.wrappedValue.addExpenses()
}
ForEach(a.expenses) { b in
if a.expenses.isEmpty == false {
TextField("my expense", text: b.expensetype)
TextField("amount", value: b.amount, format: .number)
Button("Add image") {
withAnimation {
showingOptions = true
}
}
.confirmationDialog("", isPresented: $showingOptions, titleVisibility: .hidden) {
Button("Take photo") {
self.sourceType = .camera
self.isImagePickerDisplay.toggle()
}
Button("Choose photo") {
self.sourceType = .photoLibrary
self.isImagePickerDisplay.toggle()
}
}
if selectedImage != nil {
Image(uiImage: self.selectedImage!)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 150, height: 150)
} else {
Image(systemName: "snow")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 150, height: 150)
}
}
}
}
}
}
}
.sheet(isPresented: self.$isImagePickerDisplay) {
ImagePickerView(selectedImage: self.$selectedImage, sourceType: self.sourceType)
}
}
}
func addCard() {
cards.append(Card(title: title, expenses: []))
}
}
Here is my ImagePickerView
struct ImagePickerView: UIViewControllerRepresentable {
#Binding var selectedImage: UIImage?
#Environment(\.presentationMode) var isPresented
var sourceType: UIImagePickerController.SourceType
func makeUIViewController(context: Context) -> some UIViewController {
let imagePicker = UIImagePickerController()
imagePicker.sourceType = self.sourceType
imagePicker.delegate = context.coordinator
return imagePicker
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
}
func makeCoordinator() -> Coordinator {
return Coordinator(picker: self)
}
}

How does SwiftUI know which view's needed to redraw or not?

I have a question about process that SwiftUI does.
This code below is from a Stanford cs193p lecture.
// View
struct ContentView: View {
#ObservedObject var viewModel: EmojiMemoryGame
var body: some View {
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 65, maximum: 100))]) {
// Set break point 1
ForEach(viewModel.cards) { card in
CardView(card: card)
.aspectRatio(2/3, contentMode: .fit)
.onTapGesture {
viewModel.choose(card)
}
}
}
}
.foregroundColor(.red)
.padding(.horizontal)
}
}
struct CardView: View {
let card: MemoryGame<String>.Card
var body: some View {
// Set break point 2
ZStack {
let shape = RoundedRectangle(cornerRadius: 20)
if card.isFaceUp {
shape.fill().foregroundColor(.white)
shape.strokeBorder(lineWidth: 3)
Text(card.content).font(.largeTitle)
} else if card.isMatched {
shape.opacity(0)
} else {
shape.fill()
}
}
}
}
// ViewModel
class EmojiMemoryGame: ObservableObject {
#Published private var model: MemoryGame<String> = EmojiMemoryGame.createMemoryGame()
static let emojis = ["๐Ÿ‘", "๐Ÿ–ฅ", "โ™ฅ๏ธ", "โ™ฆ๏ธ", "โ™ฃ๏ธ", "โ™ ๏ธ", "๐ŸŽ", "โŒ", "๐Ÿง‘๐Ÿปโ€๐Ÿ’ป", "๐Ÿ’ผ",
"รท", "โœ•", "โˆš", "๐Ÿ˜ญ", "โžก๏ธ", "๐Ÿ™Œ", "๐Ÿ†š", "๐Ÿ…ถ", "๐Ÿ“", "๐Ÿคœ"]
static func createMemoryGame() -> MemoryGame<String> {
return MemoryGame<String>(numberOfPairsOfCards: EmojiMemoryGame.emojis.count) { pairIndex in
EmojiMemoryGame.emojis[pairIndex]
}
}
var cards: [MemoryGame<String>.Card] {
return model.cards
}
func choose(_ card: MemoryGame<String>.Card) {
model.choose(card)
}
}
// Model
struct MemoryGame<CardContent: Equatable> {
private(set) var cards: [Card]
private var indexOfTheOneAndOnlyFaceUpCard: Int?
init(numberOfPairsOfCards: Int, createCardContent: (Int) -> CardContent) {
cards = []
for pairIndex in 0..<numberOfPairsOfCards {
let content = createCardContent(pairIndex)
cards.append(Card(content: content))
cards.append(Card(content: content))
}
}
mutating func choose(_ card: Card) {
if let chosenIndex = cards.firstIndex(where: { $0.id == card.id }),
!cards[chosenIndex].isFaceUp,
!cards[chosenIndex].isMatched
{
if let potentialMatchIndex = indexOfTheOneAndOnlyFaceUpCard {
if cards[chosenIndex].content == cards[potentialMatchIndex].content {
cards[chosenIndex].isMatched = true
cards[potentialMatchIndex].isMatched = true
}
indexOfTheOneAndOnlyFaceUpCard = nil
} else {
for index in cards.indices {
cards[index].isFaceUp = false
}
indexOfTheOneAndOnlyFaceUpCard = chosenIndex
}
cards[chosenIndex].isFaceUp.toggle()
}
}
struct Card: Identifiable {
let id: UUID = UUID()
var isFaceUp: Bool = false
var isMatched: Bool = false
var content: CardContent
}
}
As far as I know, viewModel.choose(card) is called, then some Card in cards(in MemoryGame) is changed. And next model(in EmojiMemoryGame) notices that I was changed, so finally ContentView will redraw the body(specifically some part that is depending on viewModel).
So I thought all CardView is going to be redrawn, but it's not. They are redrawing CardView only that has changed.
To double check, I set the break point like the comment in code above. It turns out, first break point came up for every card, but second break point just came up for just the card that had changed before.
Why does this happen like that?
Is it just SwiftUI's power?

Does LazyHStack (SwiftUI) release memory for views out of screen?

I am trying to build a simple horizontal list of images. The images are loaded asynchronously, nothing fancy so far.
The main issue is that when scrolling and loading new images, the memory increases and never gets released. In case the images are really big, or the list of items very long, the app can crash while scrolling.
Shouldn't the LazyHStack do some memory management of views? I can not imagine this is not resolved so far, since it seems to me a very basic "feature".
The code (nothing special):
struct ThumbnailsCollectionView: View {
var imageService = ImageService.shared
#Binding var selectedIndex: Int
#State var image: UIImage?
var body: some View {
ScrollView([.horizontal]){
ScrollViewReader { reader in
LazyHStack(alignment: .bottom, spacing: 5){
ForEach((0...imageService.imageCount()-1), id: \.self) { index in
ThumbnailImageView(index: index)
}
}
}.padding(.vertical, 10)
}
}
}
struct ThumbnailImageView: View {
var imageService = ImageService.shared
var index: Int
#State var image: UIImage? = nil
var body: some View {
return Button(action: {
}) {
if let uiImage = image {
Image(uiImage: uiImage).resizable()
.aspectRatio(contentMode: .fit)
.zIndex(5)
}
else {
Rectangle().frame(width: 50, height: 50, alignment: .center)
}
}
.onAppear() {
imageService.thumbnail(at: index) { (image) in
self.image = image
}
}
}
}
ImageServiceCache
class ImageService {
static let shared = ImageService()
var thumbnailCache: NSCache<NSNumber, UIImage> = {
let cache = NSCache<NSNumber,UIImage>()
cache.countLimit = 10
return cache
}()
var urls:[URL] = {
var urls = [URL]()
for i in 0...100 {
let index = i % 4
urls.append(Bundle.main.url(forResource: "\(index)", withExtension: "jpg")!)
}
return urls
}()
func imageCount() -> Int {
return urls.count
}
func thumbnail(at index: Int, completion:#escaping (UIImage)->()) {
if let imageFromCache = thumbnailCache.object(forKey: NSNumber(integerLiteral: index)) {
return completion(imageFromCache)
}
DispatchQueue.global().async {
let image = UIImage(contentsOfFile: self.urls[index].path)!
self.thumbnailCache.setObject(image, forKey: NSNumber(integerLiteral: index))
DispatchQueue.main.async {
completion(image)
}
}
}
}

Updating a Progress Value in SwithUI

I am trying to update a progress bar using the DispatchQueue.main.async (based on #Asperi's reply at but this does not work! Update progress value from fileImporter modifier in SwiftUI). The problem I am having is the progress updates waits until end of the task and then updates the progress bar which is not the expected behavior.
I put the file processing code within DispatchQueue.global().async, which executes the code right away and updates the Progress bar immediately (goes to 100% instantly) but the file processing task is still not complete. How do I get this to work the right way. Below is the code I am executing. Appreciate your help
Below is the ContentView:
struct ContentView: View {
#ObservedObject var progress = ProgressItem()
#State var importData: Bool = false
var body: some View {
VStack {
Button(action: {importData = true}, label: {Text("Import Data")})
.fileImporter(isPresented: $importData, allowedContentTypes: [UTType.plainText], allowsMultipleSelection: false) { result in
do {
guard let selectedFile: URL = try result.get().first else { return }
let secured = selectedFile.startAccessingSecurityScopedResource()
DispatchQueue.global().async { // added this to import file asynchronously
DataManager(progressBar: progress).importData(fileURL: selectedFile)
}
if secured {selectedFile.stopAccessingSecurityScopedResource()}
} catch {
print(error.localizedDescription)
}
}
ProgressBar(value: $progress.progress, message: $progress.message).frame(height: 20)
}
}
}
ProgressBar View:
struct ProgressBar: View {
#Binding var value: Double
#Binding var message: String
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .leading) {
Rectangle().frame(width: geometry.size.width , height: geometry.size.height)
.opacity(0.3)
.foregroundColor(Color(UIColor.systemTeal))
Rectangle().frame(width: min(CGFloat(self.value)*geometry.size.width, geometry.size.width), height: geometry.size.height)
.foregroundColor(Color(UIColor.systemBlue))
.animation(.linear)
//Text("\(self.value)")
}.cornerRadius(45.0)
}
}
}
DataManager that processes the file. Right now I have a thread.sleep(0..001) to simulate reading of the file
class DataManager {
#Published var progressBar: ProgressItem? = nil
init(progressBar: ProgressItem? = nil) {
self.progressBar = progressBar
}
func importData(fileURL: URL, delimeter: String = ",") {
let lines = try! String(contentsOf: fileURL, encoding: .utf8).components(separatedBy: .newlines).filter({!$0.isEmpty})
let numlines = lines.count
for index in 0..<numlines {
let line = String(lines[index])
Thread.sleep(forTimeInterval: 0.001)
print("\(line)")
DispatchQueue.main.async { // updating the progress value here
self.progressBar?.progress = Double(index)/Double(numlines) * 100
self.progressBar?.message = line
}
}
}
}
ProgressItem:
class ProgressItem: ObservableObject {
#Published var progress: Double = 0
#Published var message: String = ""
}

SwiftUI Image won't update

I'm really new to swift. What am I doing wrong? Why isn't the image updating when pressing it? I couldn't find anything about state listeners.
This is the View that bundles two views:
import SwiftUI
struct FoodContentView: View {
var body: some View {
VStack {
imageView.gesture(tap)
FoodContentTextView().position(x: bodyWidth/3, y: bodyHeight/2)
}
}
}
var imageView = FoodContentImageView(selectedImage: "CheeseBurger")
let tap = TapGesture()
.onEnded { _ in
if imageView.imageSelected == "Burger" {
imageView.imageSelected = "CheeseBurger"
print(imageView.imageSelected)
} else {
imageView.imageSelected = "Burger"
print(imageView.imageSelected)
}
}
And this is the View where the image gets defined:
struct FoodContentImageView: View {
var imageSelected: String
init(selectedImage: String){
imageSelected = selectedImage
}
var body: some View {
Image(imageSelected).resizable().frame(width: 200, height: 200, alignment: .center).scaledToFit()
}
}
You are making it very complicated unnecessarily. FoodContentImageView just needs to know the name of the image:
struct FoodContentImageView: View {
var imageName: String
var body: some View {
Image(imageName).resizable().frame(width: 200, height: 200, alignment: .center).scaledToFit()
}
}
And you just need to pass in the name. If you store it as a #State, it will automatically updates on any change:
struct ContentView: View {
#State var text = "CheeseBurger"
var body: some View {
VStack {
FoodContentImageView(imageName: text)
.onTapGesture {
if self.text == "Burger" {
self.text = "CheeseBurger"
} else {
self.text = "Burger"
}
}
}
}
}
And there is no need to reference the imageView and tapGesture at all.

Resources