OBJECTIVE
I am trying to display a UIView, UIImage, and UITextView when a function is running in order to let a user know it is processing (similar to an Activity Indicator, but more customized).
PROBLEM
When the code below is processing, the UIView, UIImage, and UITextView don't display until a moment before the function finishes running (instead of showing as soon the function starts running and hiding when the function finishes).
CURRENT APPROACH:
I have created a UIView (loadingView) which contains and image (loadingIcon) and a textView (loadingText) explaining to the user that the app is processing.
I have also created a function called isLoading, which displays or hides all 3, rather than repeating these lines multiple times. I have tested setting isLoading to true and false in the viewDidLoad to make sure that it is working properly.
#IBOutlet weak var loadingView: UIView!
#IBOutlet weak var loadingIcon: UIView!
#IBOutlet weak var loadingText: UIView!
override func viewDidLoad() {
super.viewDidLoad()
isLoading(false)
}
func isLoading(_ loadStatus: Bool) {
if loadStatus == true {
loadingView.isHidden = false
loadingIcon.isHidden = false
loadingText.isHidden = false
} else {
loadingView.isHidden = true
loadingIcon.isHidden = true
loadingText.isHidden = true
}
}
#IBAction func sendButtonPressed(_ sender: AnyObject) {
isLoading(true)
... //process information, which takes some time
isLoading(false)
}
Any help, suggestions, or thoughts are immensely appreciated. Thank you.
You are running the process on the main queue so your UI appears to hang until it is finished. You need to process the information in the background. A common pattern you might use would be:
#IBAction func sendButtonPressed(_ sender: AnyObject) {
isLoading(true)
// Do the processing in the background
DispatchQueue.global(qos: .userInitiated).async {
... //process information, which takes some time
// And update the UI on the main queue
DispatchQueue.main.async {
isLoading(false)
}
}
}
Related
Trying to set an image on a button when tapped. Verified that the targetAction handler is always invoked on the main thread.
This doesn't work
#objc func buttonTapped() {
self.mybutton.imageView?.image = MyObjcClass.getImageNamed("DarkVersion")
}
But this works:
#objc func buttonTapped() {
DispatchQueue.main.async { // Does not work without this dispatch
self.mybutton.imageView?.image = MyObjcClass.getImageNamed("DarkVersion")
}
}
On the debugger I can see that both are on the main thread
Correct way to set an image to a button is using of setImage(_ image: UIImage?,for state: UIControl.State) method.
When you use direct access to set image, the button does not know about it and change the set image in state changing routine.
Really you do not need to change image in buton's action method, you just need
set images for states and to change a state of the button in this method. Button'll pick appropriate image for state.
The correct code from the view controller should be:
class ViewController: UIViewController {
#IBOutlet weak var myButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(buttonTapped))
myButton.addGestureRecognizer(tapGesture)
}
#objc func buttonTapped() {
self.myButton.setImage( MyObjcClass().getImageNamed("DarkVersion"), for: .normal)
}
}
Which works without fail, the difference is that setImage is used to set the image.
Everything I've read says that bind(to:) calls subscribe(onNext:) within it. So I assume I should be able to swap out some stuff, but when I use `bind(to:) the thing it's binding to fires immediately. Here's my example:
ButtonCollectionViewCell
class ButtonCollectionViewCell: UICollectionViewCell {
lazy var buttonTapped: Observable<Void> = { _buttonTapped.asObservable() }()
private var _buttonTapped = PublishSubject<Void>()
private var disposeBag = DisposeBag()
#IBOutlet private weak var textLabel: UILabel!
#IBOutlet private weak var actionButton: UIButton!
// MARK: - Lifecycle
override func awakeFromNib() {
super.awakeFromNib()
actionButton.rx.tap.bind(to: _buttonTapped).disposed(by: disposeBag)
}
override func prepareForReuse() {
disposeBag = DisposeBag()
}
}
Now when I do the following below everything works as expected and it prints to the console when I tap the button
ViewController with a collection view
func createButtonCell() {
let buttonCell = ButtonCollectionViewCell() // there's more code to create it, this is just for simplicity
buttonCell.buttonTapped.subscribe { _ in
print("tapped")
}.disposed(by: disposeBag)
return buttonCell
}
However, if I change the above to:
func createButtonCell() {
let buttonCell = ButtonCollectionViewCell()
buttonCell.buttonTapped.bind(to: buttonTapped)
return buttonCell
}
private func buttonTapped(_ sender: Observable<Void>) {
print("tapped")
}
The "tapped" is printed out right before I get to the cell when scrolling which I assume is when it's being created.
I don't understand this. I thought I could almost swap out the implementations? I would like to use the second example above there as I think it's neater but can't figure out how.
Your two examples are not identical...
In the first example you have: .subscribe { _ in print("tapped") } which is not a subscribe(onNext:) call. The last closure on the subscribe is being used, not the first. I.E., you are calling subscribe(onDisposed:).
Also, your ButtonCollectionViewCell is setup wrong. You bind in the awakeFromNib() which is only called once, and dispose in the prepareForReuse() which is called multiple times. One of the two needs to be move to a more appropriate place...
UPDATE
You could either rebind your subject after reseating the disposeBag, or you could just not put the chain in the dispose bag in the first place by doing:
_ = actionButton.rx.tap
.takeUntil(rx.deallocating)
.bind(to: _buttonTapped)
My app has a UISwitch and depending on the position of the switch the app will choose which array is needed. If I start my app and go to use it without moving the switch (leaving it on the isOn position) the app will crash. (Thread 1: Fatal error: Index out of range)
As long as I move it once it will then function as I had hoped. Here is the code:
import UIKit
var selectedRunesArray = [Rune]()
class SettingsViewController: UIViewController {
#IBOutlet weak var allowReversedRunesSwitch: UISwitch!
#IBOutlet weak var allowDuplicateRunesSwitch: UISwitch!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
#IBAction func reversedRunesChanged(_ sender: UISwitch) {
if allowReversedRunesSwitch.isOn == true {
selectedRunesArray = runesIncReversedArray
} else {
selectedRunesArray = runesArray
}
}
#IBAction func duplicateRunesChanged(_ sender: UISwitch) {
if allowDuplicateRunesSwitch.isOn == true {
} else {
}
}
}
I thought maybe the issue here was that I had started the app off with selectedRunesArray being empty so decided to give it an initial value as follows:
var seletcedRunesArray = runesIncReversedArray
This stopped the app crashing when you try to use it without moving the switch but now it won't change between the 2 Arrays as I want it to depending on the switch state.
reversedRunesChanged method will not be called until you change state of switch.
so, you just need to define which array you want to load at first time in viewdidload. you can either use:
this
selectedRunesArray = runesIncReversedArray
or
selectedRunesArray = runesArray
and also you can do it by getting switch state in viewdidload method by putting this in viewdidload
if allowReversedRunesSwitch.isOn == true {
selectedRunesArray = runesIncReversedArray
} else {
selectedRunesArray = runesArray
}
I have a UIViewController (Main Menu) that is displayed after my other UIViewController (Loading Screen) finishes downloading/syncing data from the web. My Main Menu is having problems as it doesn't always display all the UIButtons that are on the view when it shows up! There is an inconsistancy, 1 out of every 10 times the Main Menu loads, all the UIButtons will appear.
Here's how the view should look:
Here's how the view usually shows up:
I can put my finger where the UIButtons should be and move my finger away and they'll appear. I can also tap where they should be and my segues will still trigger, but the UIButtons dont just automatically show up. It looks like
I attempted to add a MainMenuView.setNeedsDisplay() to the ViewDidLoad function and it didn't help. I also created a test UIButton on the view that triggers a MainMenuView.setNeedsDisplay() command but the hidden UIButtons remain hidden.
If I leave the screen idle for 30 or so seconds, all the UIButtons will randomly appear without me having to manually make them appear.
The Main Menu view is normal and all UIButtons are visible if I segue to it from any part of the app aside from my Loading Screen.
Edit: Main Menu Code - (references are Strong)
import UIKit
import CoreData
class MainMenuViewController: UIViewController {
// MARK: References
#IBOutlet var btnGasManifolds: UIButton!
#IBOutlet var btnAirCompressors: UIButton!
#IBOutlet var btnVacuumPumps: UIButton!
#IBOutlet var btnUnitConversion: UIButton!
#IBOutlet var btnCFMCalculator: UIButton!
#IBOutlet var mainMenuView: UIView!
var userData = [NSManagedObject]()
// MARK: Functions
override func viewDidLoad() {
var name = String()
for duserData in userData {
name = duserData.value(forKey: "nameFull") as! String
}
print("Main Menu\n->\(name)")
backgroundSetup()
}
override func viewDidAppear(_ animated: Bool) {
mainMenuView.setNeedsDisplay()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func btnTest(_ sender: Any) {
mainMenuView.setNeedsDisplay()
}
override func prepare(for segue: UIStoryboardSegue, sender: Any!) {
if segue.identifier == "settingsSegue" {
let destination = segue.destination as! SettingsViewController
destination.userData = userData
}
}
// Background Setup
func backgroundSetup() {
let background = UIImage(named: "Data Screens")
var imageView : UIImageView!
imageView = UIImageView(frame: view.bounds)
imageView.contentMode = UIViewContentMode.scaleAspectFill
imageView.clipsToBounds = true
imageView.image = background
imageView.center = view.center
view.addSubview(imageView)
self.view.sendSubview(toBack: imageView)
}
}
The code which creates and shows the buttons after fetching the data from server needs to run on Main UI thread. If the code is running the background thread it will not properly display the UI elements and will take some time or manual scroll before showing the controls.
Run the code on Dispatch main queue.
dispatch_async(dispatch_get_main_queue(),{
//do some work
})
OR
Override one of the methods from ViewWillLayoutSubviews and ViewDidLayoutSubviews accordingly and run your code in these methods.
I have a very simple app with two viewControllers at the moment. I have a transition between them (with a segue of course) and both screens have a lot of images and buttons with images.
What I discovered was that there is a memory leak when you alternate between the screens. I am guessing, that somehow the images are loaded again each time you open the viewController again. Around 6MB is added each time and I am pretty sure that the images are less than 1MB in total. So maybe there is something else craving all the memory?
I have 6 image Views and I use the cross dissolve transition in the default modus. I wish I could copy some code here, but it is a big project and I would not know what code would be helpful.
So my question would be: what is causing this problem (and preferable how can I fix this)? Is it the images, or maybe the segues with the transition.
Any help would be really appreciated! Thanks!
--- Edit: Here's some bits of code with the images
// variables of the bottom menu bar
#IBOutlet weak var cameraButton: UIButton!
#IBOutlet weak var redoButton: UIButton!
#IBOutlet weak var saveButton: UIButton!
#IBOutlet weak var shareButton: UIButton!
#IBOutlet weak var overLay: UIImageView!
// this function lets the statusbar disappear
override func prefersStatusBarHidden() -> Bool {
return true
}
override func viewDidLoad() {
super.viewDidLoad()
overLay.image = nil
// variables for accessing web images
let url = NSURL(string: "http://www.fteunen.com/app/overLay.png")
let urlRequest = NSURLRequest(URL: url!)
NSURLConnection.sendAsynchronousRequest(urlRequest, queue: NSOperationQueue.mainQueue(), completionHandler: {
response, data, error in
if error != nil {
println("Something happened i guess, not good!!")
} else {
//let image = UIImage(data: data)
self.overLay.image = UIImage(data: data)
}
})
}
And I don't know where I can find code from the segues. I just clicked on some options for that.
---- EDIT: The second viewController (the one with all the images) and the dismissViewController:
class ViewController2: UIViewController {
// variables of the bottom menu bar
#IBOutlet weak var cameraButton: UIButton!
#IBOutlet weak var redoButton: UIButton!
#IBOutlet weak var saveButton: UIButton!
#IBOutlet weak var shareButton: UIButton!
#IBOutlet weak var overLay: UIImageView!
// this function lets the statusbar disappear
override func prefersStatusBarHidden() -> Bool {
return true
}
override func viewDidLoad() {
super.viewDidLoad()
overLay.image = nil
// variables for accessing web images
let url = NSURL(string: "http://www.fteunen.com/app/overLay.png")
let urlRequest = NSURLRequest(URL: url!)
NSURLConnection.sendAsynchronousRequest(urlRequest, queue: NSOperationQueue.mainQueue(), completionHandler: {
response, data, error in
if error != nil {
println("Something happened i guess, not good!!")
} else {
//let image = UIImage(data: data)
self.overLay.image = UIImage(data: data)
}
})
}
override func viewDidLayoutSubviews() {
//code
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func didTakePhoto(sender: AnyObject) {
//code
})
}
override func dismissViewControllerAnimated(flag: Bool, completion: (() -> Void)?) {
dismissViewControllerAnimated(true) {
// Optional completion handler code here
}
}
#IBAction func redoPicture(sender: AnyObject) {
// code
})
}
You are leaking memory because you are continuously creating new view controllers which are all essentially piling on top of each other and downloading images. You should perform a segue and then dismiss that view controller to return to the previous view controller. In your second view controller where you are trying to segue back to the first controller call dismissViewControllerAnimated in either of the following ways:
dismissViewControllerAnimated(true, completion: nil)
or:
dismissViewControllerAnimated(true) {
// Optional completion handler code here
}
Memory Leak due to constantly creating new VC ontop of VC
Use Unwind.
In MainViewController
#IBAction unwindSegue(segue: UIStoryboardSegue, sender: UIStoryboardSegue){//any code you want executed upon return to mainVC}
In NewViewController
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?){//pass any information from New to Main here}
Then, in NewVC, simply control+drag from whichever UIButton you want to cause the unwind to NewVC's Exit symbol and select unwindSegue
*
*
NOTE: Also if you want the unwind to happen programmatically instead of from a Button. Control+drag from NewVC (yellow button) to exit(red-orange button)
, this will create an unwind segue under "Exit". Select this "Unwind segue" and in attributes inspector give it an identifier.
Now in NewVC create a function
func NameYourFunc(){performSegueWithIdentifier("TheIdentiferYouUsed", sender: self)}
and anywhere in your NewVC code when you want to perform this unwind simply call NameYourFunc()