removeFromSuperview() view Not removed from first time - ios

Ok I have this case where I insert 5 Views programmatically using this method:
let starView = UIImageView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: view.frame.size.width))
// Set Image & Alpha
starView.image = #imageLiteral(resourceName: "star")
starView.alpha = 1
starView.tag = starIndex
// Add to Super View
self.mainView.addSubview(starView)
Please note that starIndex for the 5 views are 1,2,3,4,5 consequently
It's Straightforward.
After a while when an event happens, I use another method to remove these views using this method:
func removeOldStars() {
for index in 1...5 {
if let foundView = view.viewWithTag(index) {
foundView.removeFromSuperview()
}
}
}
What happens here as a result is that the last element only "number 5" is removed. I have tried several trial and error and found this weird behavior. When I remove the view twice using the tag number it works. So for example, if I want to remove view with tag number 3 if I write
view.viewWithTag(3).removeFromSuperView()
view.viewWithTag(3).removeFromSuperView()
It works!!! if just one time it doesn't do anything. I thought maybe the view is added twice and so it need to be removed twice to notice it, but i Debugged it and no the view is added single time.
I removed the view in the main thread to be sure that its not threading issue not no its not the problem.
I would appreciate your help because this is so weird i really need to understand whats happening here.

Tags, in general, are a brittle way to reference views. As #Paulw11 mentioned, this is very likely an issue with other subviews having identical tag values.
In this case, I would hold on to instances of the UIImageViews, and then in the removeOldStars method, iterate through and call removeFromSuperview on the instance directly.
//instantiate empty array of UIImageView
var starViews = [UIImageView]()
//assuming your add method name..
func addStar() {
//your code above up to...
self.mainView.addSubview(starView)
starViews.append(starView)
}
func removeOldStars() {
for view in starViews {
view.removeFromSuperview()
//maybe explicitly de allocate the view depending
}
}

Related

Activity Indicator When Switching Tabs

Using this stackoverflow solution as a guide I have a setup where I have a UITabBarController and two tabs. When changes are made in the first tab (a UIViewController), the second tab (another UIViewController with a UITableView) needs to perform some calculations, which take a while. So I have a UIActivityIndicatorView (bundled with a UILabel) that shows up when the second tab is selected, displayed, and the UITableView data is being calculated and loaded. It all works as desired in the Simulator, but when I switch to my real device (iPhone X), the calculations occur before the second tab view controller is displayed so there's just a large pause on the first tab view controller until the calculations are done.
The scary part for me is when I started debugging this with a breakpoint before the DispatchQueue.main.async call it functioned as desired. So in desperation after hours of research and debugging, I introduced a tenth of a second usleep before the DispatchQueue.main.async call. With the usleep the problem no longer occurred. But I know that a sleep is not the correct solution, so hopefully I can explain everything fully here for some help.
Here's the flow of the logic:
The user is in the first tab controller and makes a change which will force the second tab controller to recalculate (via a "dirty" flag variable held in the tab controller).
The user hits the second tab, which activates this in the UITabController:
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
let controllerIndex = tabBarController.selectedIndex
if controllerIndex == 1 {
if let controller = tabBarController.viewControllers?[1] as? SecondViewController {
if dirty {
controller.refreshAll()
}
}
}
}
Since dirty is true, refreshAll() is called for the secondController and its implementation is this:
func refreshAll() {
showActivityIndicator()
// WHAT?!?! This usleep call makes the display of the spinner work on real devices (not needed on simulator)
usleep(100000) // One tenth of a second
DispatchQueue.main.async {
// Load new data
self.details = self.calculateDetails()
// Display new data
self.detailTableView.reloadData()
// Clean up the activityView
DispatchQueue.main.async {
self.activityView.removeFromSuperview()
}
}
}
showActivityIndicator() is implemented in the second view controller as such (activityView is a class property):
func showActivityIndicator() {
let avHeight = 50
let avWidth = 160
let activityLabel = UILabel(frame: CGRect(x: avHeight, y: 0, width: avWidth, height: avHeight))
activityLabel.text = "Calculating"
activityLabel.textColor = UIColor.white
let activityIndicator = UIActivityIndicatorView(style: .medium)
activityIndicator.frame = CGRect(x: 0, y: 0, width: avHeight, height: avHeight)
activityIndicator.color = UIColor.white
activityIndicator.startAnimating()
activityView.frame = CGRect(x: view.frame.midX - CGFloat(avWidth/2), y: view.frame.midY - CGFloat(avHeight/2), width: CGFloat(avWidth), height: CGFloat(avHeight))
activityView.layer.cornerRadius = 10
activityView.layer.masksToBounds = true
activityView.backgroundColor = UIColor.systemIndigo
activityView.addSubview(activityIndicator)
activityView.addSubview(activityLabel)
view.addSubview(activityView)
}
So in summary, the above code works as desired with the usleep call. Without the usleep call, calculations are done before the second tab view controller is displayed about 19 times out of 20 (1 in 20 times it does function as desired).
I'm using XCode 12.4, Swift 5, and both the Simulator and my real device are on iOS 14.4.
Your structure is wrong. Time consuming activity must be performed off the main thread. Your calculateDetails must be ready to work on a background thread, and should have a completion handler parameter that it calls when the work is done. For example:
func refreshAll() {
showActivityIndicator()
myBackgroundQueue.async {
self.calculateDetails(completion: {
DispatchQueue.main.async {
self.detailTableView.reloadData()
self.activityView.removeFromSuperview()
}
})
}
}
So the answer is two parts:
Part 1, as guided by matt, is that I was using the wrong thread, which I believe explains the timing issue being fixed by usleep. I have since moved to a background thread with a qos of userInitiated. It seems like the original stackoverflow solution I used as a guide is using the wrong thread as well.
Part 2, as guided by Teju Amirthi, simplified code by moving the refreshAll() call to the second controller's viewDidAppear function. This simplified my code by removing the need for the logic implemented in step 2 above in the UITabController.

Why is my button displayed but not clickable?

I've created a ViewController containing a user button, which is going to be present in several View Controllers in my application.
I'm adding this ViewController dynamically to the needed ViewControllers. The user button is shown, but it's not clickable. What am I doing wrong?
I've tried setting constraints to the view containing the button, setting the container view's frame, disabling user interaction in the container view (not in the button) and nothing seems to work
import UIKit
class ModulePageViewController: UIPageViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.addSharedButtonsSubView()
}
func addSharedButtonsSubView() {
let sharedButtons = storyboard?.instantiateViewController(withIdentifier: sharedButtonsViewControllerName)
view.addSubview((sharedButtons?.view)!)
sharedButtons?.view.frame = CGRect(x: view.frame.minX, y: view.frame.minY, width: view.frame.width, height: view.frame.height)
addChild(sharedButtons!)
sharedButtons?.didMove(toParent: self)
}
}
You can create a custom view (not ViewController) containing the button and just use it where you need in you app.
#LeCalore ...
I would recommend if you want to use a button or any more stuff on multiple View Controllers then you should just make a new ViewController with that button and whatever else you want on it then use it where ever you want.
ViewController -> Present As Pop Over (Presentation : Over Current Context)
I think that's a better approach atleast for starters.
Else, as user said ... you can make a custom view programatically and call it wherever you need that's another approach but it might give you a bit of trouble.
Open to others view if there's one better.
Gluck

Why does a function in my iOS app get called twice?

I made my very first iOS app. But there are two annoying bugs which I cannot get rid of. I hope somebody can help me!
The app is supposed to train to read musical notation. The user specifies his instrument and level (on the previous viewcontroller) and based on that, it places random notes in musical notation on the screen. The user should match those notes in textfields and the app keeps track of the score and advances a level after ten right answers.
However, somehow I'm having problems with the function which generates the random notes. The function for some reason gets called twice, the first time it generates the notes, saves them in a global variable and creates the labels with the notes. The second time, it changes the global variable but not the labels. It returns the following error message this time: 2018-09-29 23:08:37.279170+0200 MyProject[57733:4748212] Warning: Attempt to present <MyProject.ThirdViewController: 0x7fc709125890> on <MyProject.SecondViewController: 0x7fc70900fcd0> whose view is not in the window hierarchy!
Because of this, the user answers the question on the screen, but the app thinks it's the wrong answer, because it has the second answer stored.
The second time the user answers a question, the function is only called once, but the read-out from the text fields doesn't update to the new values, but keeps the same as with the first question.
Here is the code which gives the problems:
import UIKit
class ThirdViewController: UIViewController
{
// snip
func setupLabels() {
// snip
// here the random notes are created, this is function is called multiple times for some reason
let antwoord = Noten()
let antwoordReturn = antwoord.generateNoten(instrument: instrument, ijkpunt: ijkpunt, aantalNoten: aantalNoten-1)
let sleutel = antwoordReturn.0
let heleOpgave = antwoordReturn.1
print(heleOpgave)
print(PassOpgave.shared.category)
let heleOpgaveNummers = antwoordReturn.2
// snip
var a = 0
while a < aantalNoten {
// the labels are created, no problems there
let myTekstveld = UITextField(frame: CGRect(x: labelX, y: labelY + 150, width: labelWidth, height: labelHeight / 2))
myTekstveld.backgroundColor = UIColor.white
myTekstveld.textAlignment = .center
myTekstveld.placeholder = "?"
myTekstveld.keyboardType = UIKeyboardType.default
myTekstveld.borderStyle = UITextField.BorderStyle.line
myTekstveld.autocorrectionType = .no
myTekstveld.returnKeyType = UIReturnKeyType.done
myTekstveld.textColor = UIColor.init(displayP3Red: CGFloat(96.0/255.0), green: CGFloat(35.0/255.0), blue: CGFloat(123.0/255.0), alpha: 1)
myTekstveld.delegate = self as? UITextFieldDelegate
myTekstveld.tag = a + 1
view.addSubview(myTekstveld)
a += 1
labelX += labelWidth
}
// the button is created
}
override func viewDidLoad()
{
super.viewDidLoad()
// snip
setupLabels()
}
#objc func buttonAction(sender: UIButton!) {
// snip
// here the text from the text fields is read, but this only works the first time the buttonAction is called, the next times, it simply returns the first user input.
while a <= aantalNoten {
if let theLabel = view.viewWithTag(a) as? UITextField {
let tekstInput = theLabel.text!
userInput.append(tekstInput)
}
a += 1
}
// snip
setupLabels()
return
}
// snip
You have two instances of ThirdViewController when you don't mean to.
This error is very telling:
2018-09-29 23:08:37.279170+0200 MyProject[57733:4748212] Warning: Attempt to present <MyProject.ThirdViewController: 0x7fc709125890> on <MyProject.SecondViewController: 0x7fc70900fcd0> whose view is not in the window hierarchy!
This is telling you that SecondViewController is trying to create ThirdViewController when SecondViewController is not even on the screen. This suggests that the mistake is in SecondViewController (perhaps observing notifications or other behaviors when not on screen). It's possible of course that you also have two instances of SecondViewController.
I suspect you're trying to build all of this by hand rather than letting Storyboards do the work for you. That's fine, but these kinds of mistakes are a bit more common in that case. The best way to debug this further is to set some breakpoints and carefully check the address of the objects (0x7fc709125890 for example). You'll need to hunt down where you're creating an extra one.
Your genreteNoten method is being called multiple times because it is called from setupLabels which is In turn called from viewDidLoad.
viewDidLoad may be called multiple times and your code should account for that. As it says in this answer to a similar question:
If you have code that only needs to run once for your controller use -awakeFromNib.
I managed to partially solve my second problem myself (that the read-out from the text fields was not updating to the second answer) by not creating them again.
I added some code to setupLabels function to only create the text fields if there was no input already:
let myTekstveld = UITextField()
if (view.viewWithTag(a+1) as? UITextField) != nil {
}
else {
myTekstveld.frame = CGRect(x: labelX, y: labelY + 100, width: labelWidth, height: labelHeight / 2)
// snip
myTekstveld.tag = a + 1
view.addSubview(myTekstveld)
}
The app works as expected now, the only problem is that the text fields are not cleared after each question.

Collection View returns nil when called in function

I want to run a function which involves adding a sublayer to a collection view. However when I run the function the app crashes saying Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value When I print the collection view it shows up in the log as none so I know the collection view is the problem. The view has already been loaded when I call the function and I can see all of it's cells. The function is being called from another class, which I think might have something to do with the problem.
Here is the function that I am calling...
func displayCircle() {
let shapeLayer = CAShapeLayer()
shapeLayer.path = circlePath.cgPath
//change the fill color
shapeLayer.fillColor = UIColor.green.cgColor
//you can change the stroke color
shapeLayer.strokeColor = UIColor.green.cgColor
//you can change the line width
shapeLayer.lineWidth = 3.0
print(shapeLayer)
print(collectionView)
collectionView!.layer.addSublayer(shapeLayer)
}
Here is how I am calling this function from another class...
ViewController().displayCircle()
EDIT: This is my storyboard layout...
What could the problem be?
As you can see, I am using a page view controller. Hope this helps
There's likely a few issues here.
If you wanted to say ViewController.displayCircle() then displayCircle would need to be a static function. But I don't think that was your intention, you probably don't want to do that in this case, and also your static function syntax is wrong (ViewController().displayCircle() is wrong). But moving on... :)
ViewController().displayCircle() isn't how you properly reference the collectionView. First you need a reference to the other view controller. Then inside displayCircle you need to grab a reference to the collectionView if it's in another View Controller. So that would be otherViewController.collectionView provided the collectionView is public of course, and provided you have a reference to that other view controller somehow. Note that you can't just make a new reference of the other view controller, otherwise you'll be adjusting the layer on the new instance, not the original.
Last but not least, you're force unwrapping the collectionView - don't do that. Your app will crash if it's ever nil. Instead, take advantage of Swift's paradigms:
if let collView = collectionView {
collView.layer...// etc
}
This last bit isn't the issue, but just good practice.
If the collectionView is part of this same viewController, use self:
self.collectionView.layer.addSublayer(shapeLayer)
If the collectionView is in a different viewController, as it seems to be, you would need to get a reference to that view controller. How you do this depends on the structure of your app. You mention using a UIPageViewController and presumably both the view controllers are presented on it.
From one view controller, you can refer to another like this:
let pvc = self.parent as? UIPageViewController // Or your custom class
let targetViewController = pvc.viewControllers[index] as? YourTargetViewControllerClass
You might need to figure out what index you need. An alternative is to make sure each child view controller of the UIPageViewController has its own subclass, then find the one you want like this:
let pvc = self.parent as? UIPageViewController
let viewControllers = pvc.viewControllers.filter { $0 is CustomSubclass }
if let viewController = viewControllers.first as? CustomSubclass {
viewController.displayCircle()
}
As the other answer states, using ViewController() creates a brand new view controller instance, not the one that already exists.
This
ViewController()
creates a new instance other than the presented one ,when you reference the collectionView from it it's actually nil as you have to load the VC either from storyboard / xib , so you have to use delegate to reference the current VC that contains the collectionView

Recognizing subview's class

I decided to animate my objects manually and therefore made an extension for UIView class:
public extension UIView{
func slideOut(){
UIView.animateWithDuration(0.5, animations: { self.frame.origin.x = -self.frame.width }, completion: finishedDisposing)
}
func finishedDisposing(successfully: Bool){
if !successfully{
((UIApplication.sharedApplication().delegate as! AppDelegate).window!.rootViewController as! VC).showSystemMessage("Failed to dispose one or more subviews from superview", ofType: .NOTICE)
}
responder.viewDisposed()
}
}
Which works nice and I have no problems about it, BUT I have a method in VC (Custom UIViewController) viewDisposed() which is called whenever a view slides out of sight and it has such an implementation:
func viewDisposed() {
disposed++
print("Updated disposed: \(disposed) / \(self.view.subviews.count)")
if disposed == self.view.subviews.count - 1{
delegate.vcFinishedDisposing()
}
}
It shows that self.view.subviews contains all my custom views + 3 more (UIView, _UILayoutGuide x 2). They do extend UIView although do not callresponder.viewDisposed method. My decision was to figure out how to get classes of each subview and Mirror(reflecting: subView).subjectType if I print it does it wonderfully. Is there any way to actually compare this to anything or, better, get String representation? Basically, I want you to help me create a method which would create a stack of subviews which are not of type UIView (only subClasses) nor _UILayoutGuide. Thank you!
You'd probably be better off directly creating an array of just the subviews you care about, instead of starting with all subviews and trying to filter out the ones you don't care about. Those layout guides weren't always there—they were added in iOS 7. Who knows what else Apple will add in the future?
Anyway:
let mySubviews = view.subviews.filter {
!["UIView", "_UILayoutGuide"].contains(NSStringFromClass($0.dynamicType))
}

Resources