Swift: Disappearing views from a stackView - ios

I have a fairly simple set up in my main storyboard:
A stack view which includes three views
The first view has a fixed height and contains a segment controller
The other two views have no restrictions, the idea being that only one will be active at a time and thus fill the space available
I have code that will deal with the changing view active views as follows:
import Foundation
import UIKit
class ViewController : UIViewController {
#IBOutlet weak var stackView: UIStackView!
#IBOutlet weak var segmentController: UISegmentedControl!
#IBAction func SegmentClicked(_ sender: AnyObject) {
updateView(segment: sender.titleForSegment(at: sender.selectedSegmentIndex)!)
}
override func viewDidLoad() {
updateView(segment: "First")
}
func updateView(segment: String) {
UIView.animate(withDuration: 1) {
if(segment == "First") {
self.stackView.arrangedSubviews[1].isHidden = false
self.stackView.arrangedSubviews[2].isHidden = true
} else {
self.stackView.arrangedSubviews[1].isHidden = true
self.stackView.arrangedSubviews[2].isHidden = false
}
print("Updating views")
print("View 1 is \(self.stackView.arrangedSubviews[1].isHidden ? "hidden" : "visible")")
print("View 2 is \(self.stackView.arrangedSubviews[2].isHidden ? "hidden" : "visible")")
}
}
}
As you can see, when the tab called 'First' is selected, the subview at index 1 should show, whilst 2 is hidden, and when anything else is selected, the subview at index 2 should show, whilst 1 is hidden.
This appears to work at first, if I go slowly changing views, but if I go a bit quicker, the view at index 1 seems to remain permanently hidden after a few clicks, resulting in the view at index 0 covering the whole screen. I've placed an animation showing the issue and a screenshot of the storyboard below. The output shows that when the problem happens, both views remain hidden when clicking on the first segment.
Can anybody tell me why this is happening? Is this a bug, or am I not doing something I should be?
Many thanks in advance!
Update: I seem to be able to reliably reproduce the issue by going to the First > Second > Third > Second > First segments in that order.

The bug is that hiding and showing views in a stack view is cumulative. Weird Apple bug. If you hide a view in a stack view twice, you need to show it twice to get it back. If you show it three times, you need to hide it three times to actually hide it (assuming it was hidden to start).
This is independent of using animation.
So if you do something like this in your code, only hiding a view if it's visible, you'll avoid this problem:
if !myView.isHidden {
myView.isHidden = true
}

Building on the nice answer by Dave Batton, you can also add a UIView extension to make the call site a bit cleaner, IMO.
extension UIView {
var isHiddenInStackView: Bool {
get {
return isHidden
}
set {
if isHidden != newValue {
isHidden = newValue
}
}
}
}
Then you can call stackView.subviews[someIndex].isHiddenInStackView = false which is helpful if you have multiple views to manage within your stack view versus a bunch of if statements.

In the end, after trying all the suggestions here I still couldn't work out why it was behaving like this so I got in touch with Apple who asked me to file a bug report. I did however find a work around, by unhiding both views first, which solved my problem:
func updateView(segment: String) {
UIView.animate(withDuration: 1) {
self.stackView.arrangedSubviews[1].isHidden = false
self.stackView.arrangedSubviews[2].isHidden = false
if(segment == "First") {
self.stackView.arrangedSubviews[2].isHidden = true
} else {
self.stackView.arrangedSubviews[1].isHidden = true
}
}
}

Based on what I can see, this weird behavior is caused by the animation duration. As you can see, it takes one second for the animation to complete, but if you start switching the segmentControl faster than that, then I would argue that is what is causing this behavior.
What you should do is deactivate the user interactivity when the method is called, and then re-enable it once the animation is complete.
It should look something like this:
func updateView(segment: String) {
segmentControl.userInteractionEnabled = false
UIView.animateWithDuration(1.0, animations: {
if(segment == "First") {
self.stackView.arrangedSubviews[1].isHidden = false
self.stackView.arrangedSubviews[2].isHidden = true
} else {
self.stackView.arrangedSubviews[1].isHidden = true
self.stackView.arrangedSubviews[2].isHidden = false
}
print("Updating views")
print("View 1 is \(self.stackView.arrangedSubviews[1].isHidden ? "hidden" : "visible")")
print("View 2 is \(self.stackView.arrangedSubviews[2].isHidden ? "hidden" : "visible")")
}, completion: {(finished: Bool) in
segmentControl.userInteractionEnabled = true
}
}
While this will prevent from fast switching (which you may see as a downside), the only other way I am aware of that solve this is by removing the animations altogether.

Check the configuration and autolayout constraints on the stack view and the subviews, particularly the segmented control.
The segmented control complicates the setup for the stack view, so I'd take the segmented control out of the stack view and set its constraints relative to the main view.
With the segmented control out of the stack view, it's relatively straightforward to set up the stack view so that your code will work properly.
Reset the constraints on the stack view so that it is positioned below the segmented control and covers the rest of the superview. In the Attributes Inspector, set Alignment to Fill, Distribution to Fill Equally, and Content Mode to Scale to Fill.
Remove the constraints on the subviews and set their Content Mode to Scale to Fill.
Adjust the indexing on arrangedSubviews in your code and it should work automagically.

Related

UISheetPresentationController displaying differently on different phones

I created a UIStoryboardSegue to make a "Bottom Sheet segue". Our designer shared a screenshot of the app on his phone and the bottom sheet is displaying differently, despite the fact we are both on the same iOS version.
On mine and my simulator, when the bottom sheet opens, it lightens the source view and then shrinks it down a little, so it appears just barely behind the bottom sheet
On the same screen on the designers device, it dims the background and leaves the source view full size, showing the top of the buttons in the navigation bar
I've noticed the Apple maps bottom sheet behaves like the designers, no shrinking of the background view. But I can't see any settings that would affect this. How can I stop the sheet from resizing the source view on mine and function like it's supposed to?
Here's my code:
import UIKit
public class BottomSheetLargeSegue: UIStoryboardSegue {
override public func perform() {
guard let dest = destination.presentationController as? UISheetPresentationController else {
return
}
dest.detents = [.large()]
dest.prefersGrabberVisible = true
dest.preferredCornerRadius = 30
source.present(destination, animated: true)
}
}
Found a hack to force it to never minimise the source view at least, not really what I wanted, but keeps it consistent. Supposedly, .large() is always supposed to minimize the source view, you can avoid this in iOS 16 by creating a custom detent that is just a tiny bit smaller than large like so:
let customId = UISheetPresentationController.Detent.Identifier("large-minus-background-effect")
let customDetent = UISheetPresentationController.Detent.custom(identifier: customId) { context in
return context.maximumDetentValue - 0.1
}
dest.detents = [customDetent]
And as a bonus, found a way to control the dimming on the bottom sheet overlay. There is a containerView property on the presentationController, but it is nil when trying to access it in while in the segue. If you force code to run on the main thread, after the call to present, you can access the containerView and set your own color / color animation
e.g.
...
...
source.present(destination, animated: true)
DispatchQueue.main.async {
dest.containerView?.backgroundColor = .white
}

Show or hide items by clicking button

I have four imageview contents in an XIB and a button that covers all my XIB. I want to make when the user tap the button, the first imageview is shown, the next tap is hidden and the second imageview is displayed and so on until all my imageview is shown / hidden. What would be the most efficient way to do it?
Save all your UIImageViews to an array, and current showing imageView to a variable, it may look like this:
var imageViews: [UIImageView] = []
var currentImageViewIndex = 0 {
didSet {
if currentImageViewIndex >= imageViews.count { currentImageViewIndex = 0 }
imageViews[oldValue].isHidden = true
imageViews[currentImageViewIndex].isHidden = false
}
}
func handleTap() {
currentImageViewIndex += 1
}
I suggest you use a state variable that contains an enum listing the various states (firstImageVisible, secondImage.... ) then you can have a function inside the enum that switches to the nextState (being the target of your button action) you can also easily iterate through states of an enum, check the documentation for the CaseIterable protocol. Often having a property observer (didSet) on the state is a handy place to update other parts of the UI which need to change every time the state changes.

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))
}

UITableView:reloadSectionIndexTitles animated correct way

I am trying to hide the index bar of a UITableView while scrolling.
Therefore I am reloading the section index titles when I start scrolling and when finish. Returning an empty array hides the bar.
My code is:
var showSectionIndexTitles = true
override func scrollViewWillBeginDragging(scrollView: UIScrollView) {
showSectionIndexTitles = false
UIView.animateWithDuration(0.5, animations: { () -> Void in
self.tableView.reloadSectionIndexTitles()
})
}
override func scrollViewWillBeginDecelerating(scrollView: UIScrollView) {
showSectionIndexTitles = true
UIView.animateWithDuration(0.5, animations: { () -> Void in
self.tableView.reloadSectionIndexTitles()
})
}
override func sectionIndexTitlesForTableView(tableView: UITableView) -> [AnyObject]! {
if showSectionIndexTitles {
return uniq([UITableViewIndexSearch] + AlphabetUppercase + datamanager.categoryIndexTitles)
} else {
return nil
}
}
This works when using no animations, but I would like to use an animation.
I would prefer an animation where the whole bar moves out to the right when the bar is hidden and move in from the right when the bar is visible
I tried to use UIView:animateWithDuration to test if an animation is possible at all.
What I have noticed:
This basic animation moves/scales in from the left top corner when
visible
When hiding the bar it disappears instantly
My questions:
What is the best way of achieving an animation for indexbar visibility?
What is the best way of achieving the particular animation I mentioned earlier?
Thank you in advance!
EDIT 1:
I just remembered where I have seen this effect before: iOS 8.4 Music App
Apple does the same when you scroll so far that you can only see the title list(UITableView)
EDIT 2:
I filed a bug report to apple suggesting a function for changing visibility of the index bar with a animated parameter. I am going to inform you as soon as I get a response.
Even though #matt already suggested a possible solution in his answer, still if anybody else knows a different convenient way of solving this problem or has also faced this kind of feature in the past I would be glad to hear from you!
What you're trying to do is unsupported. Therefore there is no "best" or "correct" way - whatever you do will be an illegal hack. What I would do is snapshot the index bar, hide the real index bar as you are already doing (i.e. legally and normally), and animate the snapshot.

Use buttons inside subview

I'm making a multiple choice quiz game, and my goal right now is to have four buttons that refresh by spinning around with new answer choices. I think that means I need a subview that animates and re-populates with new buttons--if that's incorrect or not best, please stop me here.
At any rate, I created the subview in my storyboard, put the buttons inside it (background is blue just to see it now):
I dragged that over to my ViewController to make an IBOutlet (buttonContainer) and added this code to my ViewDidLoad:
view.addSubview(buttonContainer)
let buttonTap = UITapGestureRecognizer(target:self, action: Selector("checkAnswer"))
buttonTap.numberOfTapsRequired = 1
buttonContainer.addGestureRecognizer(buttonTap)
buttonContainer.userInteractionEnabled = true
However: When I run it in the simulator, the blue background does not appear at all, but the buttons are still disabled.
Before creating the subview, both the buttons and the function (checkAnswer) they called all worked perfectly.
You don't need any of this code if you are creating everything in storyboard. Just create a new class for the containerview and connect the buttons as an outlet collection.
For example, your button container class might look something like this:
class ButtonContainerView: UIView {
#IBOutlet var answerButtons: [UIButton]!
func rotateButtons() {
for button in answerButtons {
var context = UIGraphicsGetCurrentContext()
UIView.beginAnimations(nil, context: &context)
UIView.setAnimationCurve(UIViewAnimationCurve.Linear)
UIView.setAnimationDuration(5.0)
button.transform = CGAffineTransformRotate(button.transform, CGFloat(M_PI))
UIView.commitAnimations()
}
}
}

Resources