Why are animated calls of UIView.isHidden = false compounded? - ios

As you can see I'm having trouble formulating the question. Let me try to explain:
I'm using a search bar in my swift ios app. In order to get a desired animation effect I put it in a vertical stack view and then animate its isHidden property. This way search bar pushes down other children of the stack view as it animates in or pulls them up as it animates out. So far so good.
I've noticed a behavior that I think is strange. Could be a bug or could be me not understanding how things work. Basically if I call search bar hiding method x times in a row I need to call search bar showing method x times before it would show. I'd expect to have to make just one call to show search bar regardless of how many times I called hiding method. The issue doesn't exist other way around: if I call search bar showing code x times I only need to call hiding method once for it to go away. This doesn't happen if I set isHidden without animating it...
Here's a sample code and a video of the issue. I'd appreciate it if someone would help me understand this behavior.
class ViewController: UIViewController {
#IBOutlet weak var searchBar: UISearchBar! {
didSet {
searchBar.isHidden = true
}
}
#IBAction func showAction(_ sender: UIButton) {
expandSearch()
}
#IBAction func hideAction(_ sender: UIButton) {
collapseSearch()
}
private func expandSearch() {
UIView.animate(withDuration: 0.3){
self.searchBar.isHidden = false
}
}
private func collapseSearch() {
UIView.animate(withDuration: 0.3){
self.searchBar.isHidden = true
}
searchBar.resignFirstResponder()
}
}

You should not call an asynchronous animation of searchbar x times, instead of I suggest you to keep its state in variable, something like isSearchBarHidden,
and check it before show/hide search bar. You could use just one method with such signature showSearchBar(show: Bool) and setting this variable there.
#IBAction func showAction(_ sender: UIButton) {
showSearchBar(true)
}
#IBAction func hideAction(_ sender: UIButton) {
showSearchBar(false)
}
private
func showSearchBar(_ show: Bool) {
guard isSearchBarHidden != show else {
return
}
UIView.animate(withDuration: 0.3, animations: {
self.searchBar.isHidden = show
}) {
self.isSearchBarHidden = show
if !show && searchBar.isFerstResponder {
searchBar.resignFirstResponder
}
}
}
private
var isSearchBarHidden: Bool = true
Also it is a good practice to check if your textView/textField/searchBar isFirstResponder before call resignFirstResponder.
Hope it will help. Good luck!

Related

How to Focus Accessibility On A Particular Segment in A UISegmentedControl

OK. This answer helps a lot. I can select an accessibility item when a screen is shown. I simply add
UIAccessibility.post(notification: .layoutChanged, argument: <a reference to the UI item to receive focus>)
to the end of my viewWillAppear() method, and the item receives focus.
However, in one of my screens, the item I want to receive focus is a UISegmentedControl, and, when focused, it always selects the first item, no matter which one is selected. Since I followed the excellent suggestion here, I have an accessibility label for each item in the control, and I'd like my focus to begin on whichever segment is selected.
Is there a way to do this? As a rule, I try to avoid "hacky" solutions (like the one I just referenced), but I'm willing to consider anything.
Thanks!
UPDATE: Just to add insult to injury, I am also having an issue with the item I want selected being selected, then a second later, the screen jumps the selection to the first item. That's probably a topic for a second question.
I created a blank project as follows to reproduce the problem:
The solution is taking the selectedIndex to display the selected segment and providing the appropriate segment object for the VoiceOver notification: easy, isn't it?
I naively thought that getting the subview in the segmented control subviews array with the selectedIndex would do the job but that's definitely not possible because the subviews can move inside this array as the following snapshot highlights (red framed first element for instance):
The only way to identify a unique segment is its frame, so I pick up the segmented control index and the frame of the selected segment to pass them to the previous view controller.
That will allow to display (index) and read out (frame that identifies the object for the notification) the appropriate selected segment when this screen will appear after the transition.
Hereafter the code snippets for the view controller that contains the 'Next Screen' button:
class SOFSegmentedControl: UIViewController, UpdateSegmentedIndexDelegate {
var segmentIndex = 0
var segmentFrame = CGRect.zero
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let segueName = segue.identifier {
if (segueName == "SegmentSegue") {
if let destVC = segue.destination as? SOFSegmentedControlBis {
destVC.delegate = self
destVC.segmentIndex = segmentIndex
destVC.segmentFrame = segmentFrame
}
}
}
}
#IBAction func buttonAction(_ sender: UIButton) { self.performSegue(withIdentifier: "SegmentSegue", sender: sender) }
func updateSegmentIndex(_ index: Int, withFrame frame: CGRect) {
segmentIndex = index
segmentFrame = frame
}
}
... and for the view controller that displays the segmented control:
protocol UpdateSegmentedIndexDelegate: class {
func updateSegmentIndex(_ index: Int, withFrame frame: CGRect)
}
class SOFSegmentedControlBis: UIViewController {
#IBOutlet weak var mySegmentedControl: UISegmentedControl!
var delegate: UpdateSegmentedIndexDelegate?
var segmentFrame = CGRect.zero
var segmentIndex = 0
var segmentFrames = [Int:CGRect]()
override func viewDidLoad() {
super.viewDidLoad()
mySegmentedControl.addTarget(self,
action: #selector(segmentedControlValueChanged(_:)),
for: .valueChanged)
mySegmentedControl.selectedSegmentIndex = segmentIndex
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print(mySegmentedControl.subviews)
let sortedFrames = mySegmentedControl.subviews.sorted(by: { $0.frame.origin.x < $1.frame.origin.x})
for (index, segment) in sortedFrames.enumerated() { segmentFrames[index] = segment.frame }
if (self.segmentFrame == CGRect.zero) {
UIAccessibility.post(notification: .screenChanged,
argument: mySegmentedControl)
} else {
mySegmentedControl.subviews.forEach({
if ($0.frame == self.segmentFrame) {
UIAccessibility.post(notification: .screenChanged,
argument: $0)
}
})
}
}
#objc func segmentedControlValueChanged(_ notif: NSNotification) {
delegate?.updateSegmentIndex(mySegmentedControl.selectedSegmentIndex,
withFrame: segmentFrames[mySegmentedControl.selectedSegmentIndex]!) }
}
The final result is as follows:
Double tap to go to the next screen.
Select the next element to focus the second segment.
Double tap to select the focused element.
Get back to the previous screen thanks to the Z gesture natively known by iOS with the navigation controller. The delegate passes the index and the frame of the selected segment.
Double tap to go to the next screen.
The segment that was formerly selected is read out by VoiceOver and still selected.
You can now Focus Accessibility On A Particular Segment in A UISegmentedControl following this rationale.
I try to avoid "hacky" solutions (like the one I just referenced), but I'm willing to consider anything.
Unfortunately, this solution is a hacky one... sorry. However, it works and I couldn't find another one anywhere else: see it as a personal fix unless you get a cleaner one to share? ;o)
UPDATE... That's probably a topic for a second question.
I can't reproduce the behavior of your update: if you create a dedicated topic for this problem, please add the most detailed code and context so as to provide the most accurate solution.
i think this works~!
class VC {
let segment = UISegmentedControl()
func fucusSegment(index: Int) {
let item = segment.accessibilityElement(at: index )
UIAccessibility.post(notification: .layoutChanged, argument: item)
}
}

Keyboard is appearing and immediately disappearing when UISearchBar.becomeFirstResponder() is getting called

I have UISearchController in the navigationItem.searchController and I want to make it focus when the user selects "Search" from the menu.
So shortly, when the user is tapping on the "Search" option in the menu (UITableViewCell) it's getting the view controller that have the searchController in it and calling:
guard let navigationVC = presentingViewController as? UINavigationController else { return }
guard let documentsVC = navigationVC.topViewController as? DocumentsViewController else { return }
documentsVC.searchController.searchBar.becomeFirstResponder()
Then, the UISearchBar is getting focus, the keyboard is appearing and then it's immediately disappearing, and I don't have any code that would make it disappear (like view.endEditing()).
1 GIF is worth more than 1,000 words:
So, after many tries I got some way to make it work, but I'm sure there is a much more elegant ways to do this, so if someone think that they have better way, please post it here and I may use it and mark your answer as the correct one.
Create the function focusOnSearchBar() in YourViewController:
func focusOnSearchBar() {
let searchBar = searchController.searchBar
if searchBar.canBecomeFirstResponder {
DispatchQueue.main.async {
searchBar.becomeFirstResponder()
}
} else {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.focusOnSearchBar()
}
}
}
What it actually do is use itself recursively and check (every 0.1 sec) if searchBar.canBecomeFirstResponder. This is the problematic/not elegant thing.
Then, add this to viewDidAppear():
if focusOnSearch {
searchController.isActive = true
}
Don't forget to add extension to your ViewController for UISearchControllerDelegate (and of course, set searchController.delegate = self) and implement didPresentSearchController (that will be invoke by setting searchController.isActive = true):
extension YourViewController: UISearchControllerDelegate {
func didPresentSearchController(_ searchController: UISearchController) {
if focusOnSearch {
focusOnSearchBar()
}
}
}
Now all you have to do is to set focusOnSearch = true in the prepare(for segue:sender:).
*Note: if you want to focusOnSearchBar while you are in the same viewController of the searchBar, just set:
focusOnSearch = true
searchController.isActive = true
And it will work by itself.
Make your searchbar first responder in the viewDidLoad method. That will make sure everything is ready before focusing the search bar.

How to show FullScreenSlideshowViewController on ViewDidLoad

I am new to iOS development, I have created android app where app gets sets of images (sometimes over 100 images) from urls and loads into imageView with zoomEnabled Inside ViewPager.
Now I want to create same app for iOS, I have found this lib ImageSliderShow. Problem with this is it shows fullscreen ONLY when user did tap on selected image. I have been struggling to presentFullScreenController on viewDidLoad with no lock.
I only want image to be shown in fullScreen, example:
Select Category A From CategoryVC -> Loads ImageSlideVC, gets set of images from server, show in FullScreenView.
How can i achieve this? Adding this:
slideshow.presentFullScreenController(from: self)
on viewDidLoad didn't work:
#IBOutlet var slideshow: ImageSlideshow!
let kingfisherSource = [KingfisherSource(urlString: "https://images.unsplash.com/photo-1447746249824-4be4e1b76d66?w=1080")!,
KingfisherSource(urlString: "https://images.unsplash.com/photo-1447746249824-4be4e1b76d66?w=1080")!,
KingfisherSource(urlString: "https://images.unsplash.com/photo-1463595373836-6e0b0a8ee322?w=1080")!]
override func viewDidLoad() {
super.viewDidLoad()
slideshow.setImageInputs(kingfisherSource)
slideshow.presentFullScreenController(from: self)
}
I dont get any error, output is same as before (shows slideshow in smallview with no zoom)
Please help
EDIT
by doing in viewDidAppear, after split second view is loaded its loads into fullscreen. First its smallview then shows in fullscreen. I think i have to do something inside setImageInputs
this is what its looks like :
open func setImageInputs(_ inputs: [InputSource]) {
self.images = inputs
self.pageControl.numberOfPages = inputs.count
// in circular mode we add dummy first and last image to enable smooth scrolling
if circular && images.count > 1 {
var scImages = [InputSource]()
if let last = images.last {
scImages.append(last)
}
scImages += images
if let first = images.first {
scImages.append(first)
}
self.scrollViewImages = scImages
} else {
self.scrollViewImages = images
}
reloadScrollView()
layoutScrollView()
layoutPageControl()
setTimerIfNeeded()
}
try to do it in viewDidAppear
#IBOutlet var slideshow: ImageSlideshow!
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
slideshow.setImageInputs(kingfisherSource)
slideshow.presentFullScreenController(from: self)
}
Presenting viewController in ViewDidLoad is a bad practice.

Can we make 1 UIButton for 2 action with swift?

i want to make 2 action for a button like that.
selected and deselected action for 1 button.
#IBAction func btntouch(sender: UIButton) {
if firsttouch
{
print bla bla
change button to selected style. maybe background color.
}
else
{
}
}
how can i do that?
In case you need to split two button statuses - like ON and OFF, try this:
var buttonSwitched : Bool = false
#IBAction func btntouch(sender: UIButton) {
//this line toggle your button status variable
//if true, it goes to false, and vice versa
self.buttonSwitched = !self.buttonSwitched
if self.buttonSwitched
{
//your UI styling
}
else
{
//your opposite UI styling
}
}
Create 2 IBActions:
#IBAction func touchDown(_ sender: AnyObject) {
print("down")
}
#IBAction func touchUp(_ sender: AnyObject) {
print("up")
}
When connecting the first one, make sure the event is set to touchDown. For the second one, make sure it is set to touchUpInside
Yes, you can. Depending on your requirements, you could store the current state of the button in the view controller or in the model.
If the visual change caused by the first touch needs to be persisted across opening and closing of your view controller, store the value indicating the change in the model; if you need to reset the visuals when the view controller shows, store the value in the view controller itself:
var firstTouch = true
#IBAction func btntouch(sender: UIButton) {
if firstTouch {
firstTouch = false
...
} else {
...
}
}

How to pause an animation in SWIFT?

I have seen several answers here on stackoverflow but they are all diferent to what I really need, and also they are all in Objective C...
So what I have is an animation of an object called stick that starts up and then goes down to the screen. When I press a button, I want it to pause and I want the stick to be precisely at that spot!
Also, hand is the button that stops the animation, that only stops when hand and stick and overlap.
Here's my code:
#IBAction func touchHand(sender: UIButton) {
let stickPresentationFrame = (stick.layer.presentationLayer() as! CALayer).frame
if !CGRectIsNull(CGRectIntersection(stickPresentationFrame, hand.frame)){
println("sup")
}
}
override func viewDidAppear(animated: Bool) {
UIView.animateWithDuration(2, animations: {self.stick.center = CGPointMake(self.stick.center.x, 760)}, completion: nil)
}
Any ideas? Thanks

Resources