Updating loading bar from long task - ios

I'm trying to make a loading bar as I complete a long function can could take several seconds. However I'm not sure how I can figure out how far along in the functions execution I am and how to trigger the changes in the UI. Below is what I have which works ok, but I won't behave exactly as desired. Any suggestions?
func build(_ sender: UIButton) {
let myMutableString = NSMutableAttributedString(string: "Compiling Latest Clean Build", attributes: [NSFontAttributeName: buildLbl.font])
buildLbl.attributedText = myMutableString
let greenStrip = UIView()
greenStrip.backgroundColor = Colors().green
greenStrip.frame = CGRect(x: 0, y: buildStatus.frame.height - 2.5, width: 10, height: 2.5)
buildStatus.addSubview(greenStrip)
UIView.animate(withDuration: 2.0, animations: {
// compile can take a while
compile(structureContent: structuredContent)
greenStrip.frame = CGRect(x: 0, y: self.buildStatus.frame.height - 2.5, width: self.buildStatus.frame.width, height: 2.5)
}, completion: {
(value: Bool) in
greenStrip.removeFromSuperview()
let dvc : PreviewViewController = self.storyboard?.instantiateViewController(withIdentifier: "PreviewViewController") as! PreviewViewController
self.navigationController?.pushViewController(dvc, animated: false)
})
}

You want to keep track of progress made from your compile() function that is looping through a data structure and do stuff. First, you need a delegate function from your compile() function that reports the progress as percentage so that you can later display that on UI. You can find a complete tutorial about delegate here and your protocol will look something like
protocol ProgressDelegate: class {
func didFinishTask(progress: Double)
}
Now here is a sample way of reporting progress. At the beginning of your compile() function, you want to get a count from your data structure as total jobs. Then in each round of your for loop, you divide the current loop count by total count and then report this progress by the delegate. So your code will look something like
func compile(){
...
let totalJobs = jobs.count
let counter = 0.0
for eachJob in jobs {
counter += 1.0
...
didFinishTask(progress: counter/totalJobs)
}
}
Finally, in your UI view controller that receives this delegate. Update UI base one the percentage finished of your task.
func didFinishTask(progress: Double){
// set up status bar here
}

Related

Swift calling ChildVC sometimes causes fatal error

I have been trying to debug this for several days now but I can't seem to understand how it happens. I might be missing some information on how child view controllers are called.
I have a map that opens a child view controller on longpress. This works most of the time. Sometimes though, the app crashes. The console tells me:
Fatal error: Attempted to read an unowned reference but object 0x283f3cf80 was already deallocated
Note that this does not happen most of the time. It is rare and sometimes I need to trigger the long-press many times until it comes up. This is why I cannot use breakpoints.
To help me debug, I have added many prints starting from "CHECK 1" to "CHECK 16". These print in the correct order when everything works well. When it breaks, it breaks between "CHECK 14" and "CHECK 15".
I use this code to call my ChildVC:
let slideVC = TripOverviewVC(customer: myCustomer, route: myRoute, panelController: panelController!, annotationManager: annotationManager!, mapview: mapView!)
slideVC.view.roundCorners([.topLeft, .topRight], radius: 22)
slideVC.view.layer.zPosition = 15
print("CHECK 8")
add(slideVC)
print("CHECK 13")
let height = view.frame.height
let width = view.frame.width
slideVC.view.frame = CGRectMake(0, self.view.frame.maxY, width, height)
print("CHECK 14")
And this is my add(_ child) function:
func add(_ child: UIViewController) {
addChild(child)
print("CHECK 9")
view.addSubview(child.view)
print("CHECK 11")
child.didMove(toParent: self)
print("CHECK 12")
}
On my child VC, the viewWillAppear prints "CHECK 10" which is called correctly. However, after the child has been added using my add function, I guess that viewDidAppear should be called. But it never does. This is my viewDidAppear:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print("CHECK 15")
UIView.animate(withDuration: 0.3) { [weak self] in
let frame = self?.view.frame
let yComponent = UIScreen.main.bounds.height - self!.view.frame.height*0.5
self?.view.frame = CGRectMake(0, yComponent, frame!.width, frame!.height)
}
tmpOrigin = self.view.frame.origin
print("CHECK 16")
}
This means that something breaks between adding the child as a subview, and before it is displayed on the screen, which is exactly what I can see on my app. What I don't understand is: What is being called between the two steps? Have I misunderstood the view lifecycle?
Here the force unwrapping is the problem which may lead to crash when the self is released before the execution of the animation which is delayed by 3 sec
let yComponent = UIScreen.main.bounds.height - self!.view.frame.height*0.5
self?.view.frame = CGRectMake(0, yComponent, frame!.width, frame!.height)
Instead it can be updated as
UIView.animate(withDuration: 0.3) { [weak self] in
guard let this = self else { return }
let frame = this.view.frame
let yComponent = UIScreen.main.bounds.height - this.view.frame.height *0.5
this.view.frame = CGRectMake(0, yComponent, frame.width, frame.height)
}

CABasicAnimation Sometimes Works, Sometimes Fails

I am making an app that walks the user through account creation in a series of steps. After each step is completed, the user is taken to the next view controller and a progress bar animates across the top of the screen to communicate how much of the account making process has been completed. Here is the end result:
This is accomplished by placing a navigation controller within a containing view. The progress bar is laid over the containing view and every time the navigation controller pushes a new view controller, it tells the containing view controller to animate the progress bar to a certain percentage of the superview's width. This is done through the following updateProgressBar function.
import UIKit
class ContainerVC: UIViewController {
var progressBar: CAShapeLayer!
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
progressBar = CAShapeLayer()
progressBar.bounds = CGRect(x: 0, y: 0, width: 0, height: view.safeAreaInsets.top)
progressBar.position = CGPoint(x: 0, y: 0)
progressBar.anchorPoint = CGPoint(x: 0, y: 0)
progressBar.backgroundColor = UIColor.secondaryColor.cgColor
view.layer.addSublayer(progressBar)
}
func updateProgressBar(to percentAsDecimal: CGFloat!) {
let newWidth = view.bounds.width * percentAsDecimal
CATransaction.begin()
CATransaction.setCompletionBlock({
self.progressBar.bounds.size.width = newWidth
})
CATransaction.commit()
let anim = CABasicAnimation(keyPath: "bounds")
anim.isRemovedOnCompletion = true
anim.duration = 0.25
anim.fromValue = NSValue(cgRect: CGRect(x: 0, y: 0, width: progressBar.bounds.width, height: view.safeAreaInsets.top))
anim.toValue = NSValue(cgRect: CGRect(x: 0, y: 0, width: newWidth, height: view.safeAreaInsets.top))
progressBar.add(anim, forKey: "anim")
}
}
The view controllers in the navigation controller's stack will call this updateProgressBar function when pushing the next VC. This is done like so:
class FourthViewController: UIViewController {
var containerVC: ContainerViewController!
...
#IBAction func nextButtonPressed(_ sender: Any) {
let storyboard = UIStoryboard(name: "Main", bundle: .main)
let fifthVC = storyboard.instantiateViewController(withIdentifier: "FifthVC") as! FifthViewController
fifthVC.containerVC = containerVC
navigationController!.pushViewController(fifthVC, animated: true)
//We pass 5/11 because the next step will be step 5 out of 11 total steps
self.containerVC.updateProgressBar(to: 5/11)
}
}
Similarly, when pressing the back button, we shrink the container VC's progress bar:
class FourthViewController: UIViewController {
var containerVC: ContainerViewController!
...
#IBAction func backButtonPressed(_ sender: Any) {
navigationController!.popViewController(animated: true)
//We pass 3/11 because the previous step is step 3 out of 11 total steps
containerVC.updateProgressBar(to: 3/11)
}
}
My problem is that this animation only sometimes works. The progress bar always works when moving forward in the process, but sometimes, when a user navigates back, the bar gets stuck and will no longer move in either direction until an unreached view controller is presented. See the video below:
Video of Bug (Bug begins around 0:23)
I have confirmed that the presentation of an Alert Controller is not the cause of the failure to animate, and have also made sure that the animation is occurring on the main thread. Any suggestions?
As very well explained in this answer here, viewDidLayoutSubviews() gets called more than once.
In your case, you're ending up instatiating a new CAShapeLayer every time you push or pop a view controller from the navigation stack.
Try using viewDidAppear() instead.

Can't Get iOS Print Renderer to Draw Properly

OK. There seems to be a dearth of examples on this, and I am fairly stumped.
I'm trying to make a custom print page renderer; the type that completely customizes the output, not one that uses an existing view.
The really weird thing, is that I was able to do this in ObjC a couple of years ago, and I can't seem to do the same thing in Swift.
I should mention that I am using the prerelease (Beta 5) of Xcode, and Swift 4 (Which has almost no difference at all from Swift 3, in my project).
The project is here.
It's a completely open-source project, so nothing's hidden; however, it's still very much under development, and is a moving target.
This is the page renderer class.
BTW: Ignore the delegate class. I was just thrashing around, trying to figure stuff up. I'm not [yet] planning on doing any delegate stuff.
In particular, my question concerns what's happening here:
override func drawContentForPage(at pageIndex: Int, in contentRect: CGRect) {
let perMeetingHeight: CGFloat = self.printableRect.size.height / CGFloat(self.actualNumberOfMeetingsPerPage)
let startingPoint = max(self.maxMeetingsPerPage * pageIndex, 0)
let endingPointPlusOne = min(self.maxMeetingsPerPage, self.actualNumberOfMeetingsPerPage)
for index in startingPoint..<endingPointPlusOne {
let top = self.printableRect.origin.y + (CGFloat(index) * perMeetingHeight)
let meetingRect = CGRect(x: self.printableRect.origin.x, y: top, width: self.printableRect.size.width, height: perMeetingHeight)
self.drawMeeting(at: index, in: meetingRect)
}
}
and here:
func drawMeeting(at meetingIndex: Int, in contentRect: CGRect) {
let myMeetingObject = self.meetings[meetingIndex]
var top: CGFloat = contentRect.origin.y
let topLabelRect = CGRect(x: contentRect.origin.x, y: 0, width: contentRect.size.width, height: self.meetingNameHeight)
top += self.meetingNameHeight
let meetingNameLabel = UILabel(frame: topLabelRect)
meetingNameLabel.backgroundColor = UIColor.clear
meetingNameLabel.font = UIFont.boldSystemFont(ofSize: 30)
meetingNameLabel.textAlignment = .center
meetingNameLabel.textColor = UIColor.black
meetingNameLabel.text = myMeetingObject.name
meetingNameLabel.draw(topLabelRect)
}
Which is all called from here:
#IBAction override func actionButtonHit(_ sender: Any) {
let sharedPrintController = UIPrintInteractionController.shared
let printInfo = UIPrintInfo(dictionary:nil)
printInfo.outputType = UIPrintInfoOutputType.general
printInfo.jobName = "print Job"
sharedPrintController.printPageRenderer = BMLT_MeetingSearch_PageRenderer(meetings: self.searchResults)
sharedPrintController.present(from: self.view.frame, in: self.view, animated: false, completionHandler: nil)
}
What's going on, is that everything on a page is being piled at the top. I am trying to print a sequential list of meetings down a page, but they are all getting drawn at the y=0 spot, like so:
This should be a list of meeting names, running down the page.
The way to get here, is to start the app, wait until it's done connecting to the server, then bang the big button. You'll get a list, and press the "Action" button at the top of the screen.
I haven't bothered to go beyond the preview, as that isn't even working. The list is the only one I have wired up right now, and I'm just at the stage of simply printing the meeting names to make sure I have the basic layout right.
Which I obviously don't.
Any ideas?
All right. I figured out what the issue was.
I was trying to do this using UIKit routines, which assume a fairly high-level drawing context. The drawText(in: CGRect) thing was my lightbulb.
I need to do everything using lower-level, context-based drawing, and leave UIKit out of it.
Here's how I implement the drawMeeting routine now (I have changed what I draw to display more relevant information). I'm still working on it, and it will get larger:
func drawMeeting(at meetingIndex: Int, in contentRect: CGRect) {
let myMeetingObject = self.meetings[meetingIndex]
var remainingRect = contentRect
if (1 < self.meetings.count) && (0 == meetingIndex % 2) {
if let drawingContext = UIGraphicsGetCurrentContext() {
drawingContext.setFillColor(UIColor.black.withAlphaComponent(0.075).cgColor)
drawingContext.fill(contentRect)
}
}
var attributes: [NSAttributedStringKey : Any] = [:]
attributes[NSAttributedStringKey.font] = UIFont.italicSystemFont(ofSize: 12)
attributes[NSAttributedStringKey.backgroundColor] = UIColor.clear
attributes[NSAttributedStringKey.foregroundColor] = UIColor.black
let descriptionString = NSAttributedString(string: myMeetingObject.description, attributes: attributes)
let descriptionSize = contentRect.size
var stringRect = descriptionString.boundingRect(with: descriptionSize, options: [NSStringDrawingOptions.usesLineFragmentOrigin,NSStringDrawingOptions.usesFontLeading], context: nil)
stringRect.origin = contentRect.origin
descriptionString.draw(at: stringRect.origin)
remainingRect.origin.y -= stringRect.size.height
}

How to stop ViewController in Swift?

my ViewController is still sending an array update, even if I'm in another View, what can I do? Here is my code:
import UIKit
import CoreBluetooth
class ViewController: UIViewController {
var audioVibe : AudioVibes!
var superpowered:Superpowered!
var displayLink:CADisplayLink!
var layers:[CALayer]!
var magnitudeArray : [UInt16] = [0, 0, 0, 0, 0, 0, 0]
override func viewDidLoad() {
super.viewDidLoad()
// Setup 8 layers for frequency bars.
let color:CGColorRef = UIColor(red: 0, green: 0.6, blue: 0.8, alpha: 1).CGColor
layers = [CALayer(), CALayer(), CALayer(), CALayer(), CALayer(), CALayer(), CALayer(), CALayer()]
for n in 0...7 {
layers[n].backgroundColor = color
layers[n].frame = CGRectZero
self.view.layer.addSublayer(layers[n])
}
superpowered = Superpowered()
// A display link calls us on every frame (60 fps).
displayLink = CADisplayLink(target: self, selector: #selector(ViewController.onDisplayLink))
displayLink.frameInterval = 1
displayLink.addToRunLoop(NSRunLoop.currentRunLoop(), forMode: NSRunLoopCommonModes)
}
// Gets triggered when you leave the ViewController.
override func viewWillDisappear(animated: Bool) {
superpowered.togglePlayback();
superpowered.moritz();
//delete(superpowered);
}
func onDisplayLink() {
// Get the frequency values.
let magnitudes = UnsafeMutablePointer<Float>.alloc(8)
superpowered.getMagnitudes(magnitudes)
// Wrapping the UI changes in a CATransaction block like this prevents animation/smoothing.
CATransaction.begin()
CATransaction.setAnimationDuration(0)
CATransaction.setDisableActions(true)
// Set the dimension of every frequency bar.
let originY:CGFloat = self.view.frame.size.height - 20
let width:CGFloat = (self.view.frame.size.width - 47) / 5
var frame:CGRect = CGRectMake(20, 0, width, 0)
for n in 0...4 {
frame.size.height = CGFloat(magnitudes[n]) * 2000
frame.origin.y = originY - frame.size.height
layers[n].frame = frame
frame.origin.x += width + 1
}
// Set the magnitudes in the array.
for n in 0...6 {
magnitudeArray[n] = UInt16(magnitudes[n] * 32768)
}
// Update the array in the audioVibe class to trigger the sending command.
audioVibe.magnitudeArray = magnitudeArray
CATransaction.commit()
// Dealloc the magnitudes.
magnitudes.dealloc(8)
}
}
I want him to stop doing things like audioVibe.magnitudeArray = magnitudeArray while I'm not in his view, what can I do?
Thanks!
Looks like you display link isn't getting paused when you transition to different views.
You could pause your display link when transitioning away from this view and resume it when coming back to this view using the paused property on CADisplayLink. You would pause it in viewWillDisappear and resume in viewWillAppear.
In your viewDidLoad method you're creating a CADisplayLink and adding it to the run loop.
If you don't do anything, that display link will stay active when you push another view controller on top of it.
You should move the code that creates the display link and adds it to the run loop to your viewWillAppear method.
Then you need to add code in viewWillDisappear that removes the display link from the run loop, invalidates it, and nils it out.
That way, you'll start your display link code when the view appears, and stop it when the view disappears.

Swift: How can I make a animation happen only once and not every time I edit something

I am a beginner in swift. I am building a single view tip calculator app and i only want some animation to happen when the app loads for the first time. And not happen every time i edit something.
Here is my code:
I am hiding the container outside the view when the app loads
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
billView.center.y -= (billView.frame.height)/2
}
And animating it to move down
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
UIView.animateWithDuration(0.8, delay: 0.0,
options: [.CurveEaseOut], animations: {
self.billView.center.y = 210 }, completion: nil)
print(billView.center.y)
}
This works fine when the app loads. Unfortunately this animation keeps happening again and again anytime I interact with the app.
Like this animation happens when this function is called (which is when any information is entered on the text field or the slider is moved)
let billAmount = NSString(string: billField.text!).doubleValue
var tipPercentages = [0.15, 0.18, 0.22]
let tipPercentage = tipPercentages[segTipControl.selectedSegmentIndex]
let percentLabel = round(tipPercentage * 100)
selectedbillPercentageLabel.text = "+\(percentLabel)%"
let tipAmount = billAmount * tipPercentage
let totalAmount = billAmount + tipAmount
let roundedTotalAmount = round(totalAmount * 100)/100
totalLabel.text = "$\(roundedTotalAmount)"
let splitSelected = Double(splitValueSlider.value)
let intsplitSelected = Int(splitSelected)
splitLabel.text = "\(intsplitSelected)"
let splitDollarAmount = roundedTotalAmount/splitSelected
let roundedSplitDollarAmount = round(splitDollarAmount * 100)/100
splitAmount.text = "$\(roundedSplitDollarAmount)"
Is there a way to avoid calling the method viewDidLayoutSubviews() when any data is entered in the app.
animated gif
Rather than avoiding calling viewDidLayourSubviews, you could add a simple boolean flip to show your animation only once. Add a boolean property that you initialize to false; show animation on condition of this property being false, and flip the boolean after the animation has been shown.
var animationHasBeenShown = false // class property
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if !animationHasBeenShown {
UIView.animateWithDuration(0.8, delay: 0.0,
options: [.CurveEaseOut], animations: {
self.billView.center.y = 210 }, completion: nil)
print(billView.center.y)
animationHasBeenShown = true
}
}
Alternatively, consider placing your animation in another context; does it really need to be in viewDidLayoutSubviews() (depends on the context of your app, but possibly rather in viewDidAppear()?)

Resources