Updating a Progress Value in SwithUI - ios

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 = ""
}

Related

how to make A SwiftUI Image Gallery/Slideshow With Auto Scrolling in SwiftUI?

How can I make a slider when my Data is coming from API? I am using
this(below code) for static images work fine but whenever I try to
use API data then my code does not work.
How to Set the Marquee in this images.
This is My code
public struct MagazineModel: Decodable {
public let magzineBanners: [MagzineBanner]
}
public struct MagzineBanner: Decodable, Identifiable {
public let id: Int
public let url: String
}
This is My View Model
//View Model for Magazines and showing Details
class MagazineBannerVM: ObservableObject{
#Published var datas = [MagzineBanner]()
let url = "ApiUrl"
init() {
getData(url: url)
}
func getData(url: String) {
guard let url = URL(string: url) else { return }
URLSession.shared.dataTask(with: url) { (data, _, _) in
if let data = data {
do {
let results = try JSONDecoder().decode(MagazineModel.self, from: data)
DispatchQueue.main.async {
self.datas = results.magzineBanners
}
}
catch {
print(error)
}
}
}.resume()
}
}
struct MagazineBannerView: View{
#ObservedObject var list = MagazineBannerVM()
public let timer = Timer.publish(every: 2, on: .main, in: .common).autoconnect()
#State var currentIndex = 0
#State var totalImages = 2
var body: some View{
ScrollView(.horizontal) {
GeometryReader { proxy in
TabView(selection: $currentIndex) {
HStack{
ForEach(list.datas, id: \.id){ item in
Group{
AsyncImage(url: URL(string: item.url)){ image in
image
.resizable()
.frame(width:UIScreen.main.bounds.width, height: 122)
}placeholder: {
Image("logo_gray").resizable()
.frame(width:UIScreen.main.bounds.width, height: 122)
}
}
}
}
}
.tabViewStyle(PageTabViewStyle())
.onReceive(timer, perform: { _ in
withAnimation{
currentIndex = currentIndex < totalImages ? currentIndex + 1: 0
}
})
}
}
}
}
I want to change images after every 2 seconds and every images has
full width as the screen width
And it is showing the half of screen width and showing both images in
single view

How to handle loading screens in SwiftUI?

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.

SwiftUI: Best approach for fetching data and passing to SubViews?

I have a parent view DetailView and I want to query for heart rates during a workout. After I get back the heart rates I want to pass them to a child View. The code below, the heart rates aren't getting passed to HeartRateRecoveryCard ?
import SwiftUI
import HealthKit
struct DetailView: View {
#State var heartRateSamples: [HKQuantitySample] = []
var body: some View {
HeartRateRecoveryCard(heartRateSamples: heartRateSamples)
.onAppear {
getHeartRatesFromWorkout()
}
}
private func getHeartRatesFromWorkout() {
guard let workout = SelectedWorkoutSingleton.sharedInstance.selectedWorkout else { return }
print("workout = \(workout.startDate)")
WorkoutManager.getHeartRateSamplesFrom(workout: workout) { (samples, error) in
guard let unwrappedSamples = samples else {
print("no HR samples")
return }
if let unwrappedError = error {
print("error attempting to get heart rates = \(unwrappedError)")
}
print("we have \(unwrappedSamples.count) heart rates")
DispatchQueue.main.async {
heartRateSamples = unwrappedSamples
}
}
}
}
struct HeartRateRecoveryCard: View {
//Don't download HR samples for this view the parent view should download HR samples and pass them into it's children views
#State var heartRateSamples: [HKQuantitySample]
#State var HRRAnchor = 0
#State var HRRTwoMinutesLater = 0
#State var heartRateRecoveryValue = 0
var body: some View {
VStack(alignment: .leading) {
Text("\(heartRateRecoveryValue) bpm")
Text("\(HRRAnchor) bpm - \(heartRateRecoveryValue) bpm")
SwiftUILineChart(chartLabelName: "Heart Rate Recovery", entries: convertHeartRatesToLineChartDataAndSetFormatter(heartRates: heartRateSamples).0, xAxisFormatter: convertHeartRatesToLineChartDataAndSetFormatter(heartRates: heartRateSamples).1)
.frame(width: 350, height: 180, alignment: .center)
.onAppear {
print("heart rate count = \(heartRateSamples.count)")
if let unwrappedHRRObject = HeartRateRecoveryManager.calculateHeartRateRecoveryForHRRGraph(heartRateSamples: heartRateSamples) {
HRRAnchor = unwrappedHRRObject.anchorHR
HRRTwoMinutesLater = unwrappedHRRObject.twoMinLaterHR
heartRateRecoveryValue = unwrappedHRRObject.hrr
}
}
}
}

Swift access progressvalue in VNRecognizeTextRequest with completion handler

I'd like to capture the progress value in a VNRecognizeTextRequest session. So I inclueded it in a closure. The problem is it is passed when the closure is completed. I can capture the value and print it but not pass it to the main thread to update my progress bar. So I pass from 0% to 100% in the main thread. Can anybody give me a hand please? Thanks a lot.
Here's my code.
private func readImage(image:UIImage, completionHandler:#escaping(([VNRecognizedText]?,Error?)->Void), comp:#escaping((Double?,Error?)->())) {
var recognizedTexts = [VNRecognizedText]()
let requestHandler = VNImageRequestHandler(cgImage: (image.cgImage)!, options: [:])
let textRequest = VNRecognizeTextRequest { (request, error) in
guard let observations = request.results as? [VNRecognizedTextObservation] else { completionHandler(nil,error)
return
}
for currentObservation in observations {
let topCandidate = currentObservation.topCandidates(1)
if let recognizedText = topCandidate.first {
recognizedTexts.append(recognizedText)
}
}
completionHandler(recognizedTexts,nil)
}
textRequest.recognitionLevel = .accurate
textRequest.recognitionLanguages = ["es"]
textRequest.usesLanguageCorrection = true
textRequest.progressHandler = {(request, value, error) in
print(value)
comp(value,nil)
}
try? requestHandler.perform([textRequest])
}
This is how I call my function from the content view.
struct ContentView: View {
#State var ima = drawPDFfromURL(url: dalai)
#State private var stepperCounter = 0
#State private var observations = [VNRecognizedText]()
#State private var progressValue: Float = 0.0
private var originaImage = drawPDFfromURL(url: dalai)
var body: some View {
VStack { Button(action: {
//self.observations = readText(image: self.ima!)
DispatchQueue.main.async {
readImage(image: self.ima!, completionHandler: { (texts, error) in
self.observations = texts!
}) { (value, err) in
self.progressValue = Float(value!)
}
}
})
{
Text("Read invoice")
}
ProgressBar(value: $progressValue).frame(height: 20)
}.padding()
}
}
This is my ProgressBar object
struct ProgressBar: View {
#Binding var value: Float
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)
}.cornerRadius(45)
}
}
}
I believe that you need to put your request in a async Thread.
DispatchQueue.global(qos: .default).async {
// Run request
perform(...)
}

Segue to a new view in SwftUI after a successful authentication from API without a NavigationButton

I am trying to segue to an entirely new view after I get a 200 OK response code from my API. The response code is successful sent back to the app and is printed into the console and the view is loaded. Except when the view loads in loads on top of the previous view. Photo of what I'm talking about. I can't use a navigation button because you can't have them preform functions and I don't want back button at the top left of the screen after a login. Here is the code I have now;
#State var username: String = ""
#State var password: String = ""
#State var sayHello = false
#State private var showingAreaView = false
#State private var showingAlert = false
#State private var alertTitle = ""
#State private var alertMessage = ""
Button(action: {
self.login()
})//end of login function
{
Image("StartNow 3").resizable()
.scaledToFit()
.padding()
}
if showingAreaView == true {
AreaView()
.animation(.easeIn)
}
//actual Login func
func login(){
let login = self.username
let passwordstring = self.password
guard let url = URL(string: "http://localhost:8000/account/auth/") else {return}
let headers = [
"Content-Type": "application/x-www-form-urlencoded",
"cache-control": "no-cache",
"Postman-Token": "89a81b3d-d5f3-4f82-8b7f-47edc39bb201"
]
let postData = NSMutableData(data: "username=\(login)".data(using: String.Encoding.utf8)!)
postData.append("&password=\(passwordstring)".data(using: String.Encoding.utf8)!)
let request = NSMutableURLRequest(url: NSURL(string: "http://localhost:8000/account/auth/")! as URL,
cachePolicy:.useProtocolCachePolicy, timeoutInterval: 10.0)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers
request.httpBody = postData as Data
let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) in
if let httpResponse = response as? HTTPURLResponse {
guard let data = data else {return}
print(data)
if httpResponse.statusCode == 200{
DispatchQueue.main.async {
//Segue to new view goes here
print(httpResponse.statusCode)
self.showingAreaView = true
}
}else{
if httpResponse.statusCode == 400{
DispatchQueue.main.async {
self.alertTitle = "Oops"
self.alertMessage = "Username or Password Incorrect"
self.showingAlert = true
print(httpResponse.statusCode)
}
}else{
DispatchQueue.main.async {
self.alertTitle = "Well Damn"
self.alertMessage = "Ay chief we have no idea what just happened but it didn't work"
self.showingAlert = true
}
}
}
do{
let JSONFromServer = try JSONSerialization.jsonObject(with: data, options: [])
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let tokenArray = try decoder.decode(token.self, from: data)
print(tokenArray.token)
UserDefaults.standard.set(tokenArray.token, forKey: "savedToken")
let savedToken = UserDefaults.standard.object(forKey: "savedToken")
print(savedToken)
}catch{
if httpResponse.statusCode == 400{
self.alertTitle = "Oops"
self.alertMessage = "Username or Password Incorrect"
self.showingAlert = true
}
print(error)
print(httpResponse.statusCode)
}
} else if let error = error {
self.alertTitle = "Well Damn"
self.alertMessage = "Ay chief we have no idea what just happened but it didn't work"
self.showingAlert = true
print(error)
}
})
dataTask.resume()
}
I've tired to google this issue but haven't had any luck. All I need is a basic segue to a new view without the need of a NavigationButton.
Any help is appreciated!
Assuming you've got two basic views (e.g., a LoginView and a MainView), you can transition between them in a couple ways. What you'll need is:
Some sort of state that determines which is being shown
Some wrapping view that will transition between two layouts when #1 changes
Some way of communicating data between the views
In this answer, I'll combine #1 & #3 in a model object, and show two examples for #2. There are lots of ways you could make this happen, so play around and see what works best for you.
Note that there is a lot of code just to style the views, so you can see what's going on. I've commented the critical bits.
Pictures (opacity method on left, offset method on right)
The model (this satisfies #1 & #3)
class LoginStateModel: ObservableObject {
// changing this will change the main view
#Published var loggedIn = false
// will store the username typed on the LoginView
#Published var username = ""
func login() {
// simulating successful API call
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
// when we log in, animate the result of setting loggedIn to true
// (i.e., animate showing MainView)
withAnimation(.default) {
self.loggedIn = true
}
}
}
}
The top-level view (this satisfies #2)
struct ContentView: View {
#ObservedObject var model = LoginStateModel()
var body: some View {
ZStack {
// just here for background
Color(UIColor.cyan).opacity(0.3)
.edgesIgnoringSafeArea(.all)
// we show either LoginView or MainView depending on our model
if model.loggedIn {
MainView()
} else {
LoginView()
}
}
// this passes the model down to descendant views
.environmentObject(model)
}
}
The default transition for adding and removing views from the view hierarchy is to change their opacity. Since we wrapped our changes to model.loggedIn in withAnimation(.default), this opacity change will happen slowly (its better on a real device than the compressed GIFs below).
Alternatively, instead of having the views fade in/out, we could have them move on/off screen using an offset. For the second example, replace the if/else block above (including the if itself) with
MainView()
.offset(x: model.loggedIn ? 0 : UIScreen.main.bounds.width, y: 0)
LoginView()
.offset(x: model.loggedIn ? -UIScreen.main.bounds.width : 0, y: 0)
The login view
struct LoginView: View {
#EnvironmentObject var model: LoginStateModel
#State private var usernameString = ""
#State private var passwordString = ""
var body: some View {
VStack(spacing: 15) {
HStack {
Text("Username")
Spacer()
TextField("Username", text: $usernameString)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
HStack {
Text("Password")
Spacer()
SecureField("Password", text: $passwordString)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
Button(action: {
// save the entered username, and try to log in
self.model.username = self.usernameString
self.model.login()
}, label: {
Text("Login")
.font(.title)
.inExpandingRectangle(Color.blue.opacity(0.6))
})
.buttonStyle(PlainButtonStyle())
}
.padding()
.inExpandingRectangle(Color.gray)
.frame(width: 300, height: 200)
}
}
Note that in a real functional login form, you'd want to do some basic input sanitation and disable/rate limit the login button so you don't get a billion server requests if someone spams the button.
For inspiration, see:
Introducing Combine (WWDC Session)
Combine in Practice (WWDC Session)
Using Combine (UIKit example, but shows how to throttle network requests)
The main view
struct MainView: View {
#EnvironmentObject var model: LoginStateModel
var body: some View {
VStack(spacing: 15) {
ZStack {
Text("Hello \(model.username)!")
.font(.title)
.inExpandingRectangle(Color.blue.opacity(0.6))
.frame(height: 60)
HStack {
Spacer()
Button(action: {
// when we log out, animate the result of setting loggedIn to false
// (i.e., animate showing LoginView)
withAnimation(.default) {
self.model.loggedIn = false
}
}, label: {
Text("Logout")
.inFittedRectangle(Color.green.opacity(0.6))
})
.buttonStyle(PlainButtonStyle())
.padding()
}
}
Text("Content")
.inExpandingRectangle(.gray)
}
.padding()
}
}
Some convenience extensions
extension View {
func inExpandingRectangle(_ color: Color) -> some View {
ZStack {
RoundedRectangle(cornerRadius: 15)
.fill(color)
self
}
}
func inFittedRectangle(_ color: Color) -> some View {
self
.padding(5)
.background(RoundedRectangle(cornerRadius: 15)
.fill(color))
}
}
You could use a flag to trigger a navigation, curently it is not possible to navigate without a NavigationLink
struct ContentView: View {
#State var logedIn = false
var body: some View {
return NavigationView {
VStack {
NavigationLink(destination: DestinationView(), isActive: $logedIn, label: {
EmptyView()
})// It won't appear on screen because the label is EmptyView
Button("log in") {
// log in logic
if succesful login {
logedIn = true // this will trigger the NavigationLink
}
}
}
}
}
}

Resources