Is there a better way to do this scrolling animation in Swift? - ios

I'm trying to add a behavior to part of my app's UI whereby the user can swipe left and this will result in a set of UILabels scrolling in sync together off-screen to the left, but then immediately scrolling back in again but from the right, but with new information contained in them.
The effect is meant to give the impression that you're moving from one "set" of info to the next... like, say, choosing a car before starting a race game... but in reality it is the same views being re-used... scrolling offscreen to have their label.text info updated... then scrolling back in again.
I have the swiping all taken care of. The issue I'm having is that my (working) solution:
UIView.animate(withDuration: 0.2, animations: {
// move label off to the left
self.titleLabel.center.x -= self.view.bounds.width
}, completion: {
$0 ; print("I'm halfway done!")
// teleport view to a location off to the right
self.titleLabel.center.x += 2*(self.view.bounds.width)
// reset label's data
self.titleLabel.text = NEW_INFO
// slide label back on screen from the right
UIView.animate(withDuration: 0.2, animations: {
self.titleLabel.center.x -= self.view.bounds.width
}, completion: nil)
})
Feels trashy like wearing someone else's underwear.
The only reason that $0 is there is to make XCode stop saying:
"Cannot convert value of type '() -> ()' to expected argument type '((Bool) -> Void)?'"
And I'm sure the fact that I'm doing the second part of the animation in a completion block will cause headaches down the road.
Is there a smarter way?
PS - I would prefer not to use any pre-made classes like "ScrollView" or anything like that... these views are all individually interactive and respond to other callbacks etc.
Thanks!

A better approach to something like this would be to use a UIPageViewController.
It will take a bit of learning and set up but is much easier than trying to roll it yourself.
The approach to take with a UIPageViewController is something like this...
Create a data model... to use your analogy...
struct Car {
let image: UIImage
let name: String
}
Then create a UIViewController subclass that will display it.
class CarViewController: UIViewController {
var car: Car? {
didSet {
displayCar()
}
}
func displayCar() {
label.text = car?.name
imageView.image = car?.image
}
}
Then you create a UIPageViewController. Inside this you have an array of cars. And in the function func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? you can then create your CarViewController and pass in the correct Car from the array.
This will then do all your scrolling and displaying and everything is still interactive.
For more information about how this works you can look at tutorials like this one from Ray Wenderlich.
You can also use this to display a part of a page (rather than scrolling the entire screen.

Related

Swift - Dark background of the ViewController that appears over the context

Here is the code that creates my custom popUp:
extension UIViewController {
public func presentPopup(animated: Bool, completion: (() -> Void)? = nil) {
let popup = MBPopUpViewController(accentColor: UIColor(hexString: "E40C15"),
popUpTitle: "Hello",
popUpMessage: "Test PopUp",
popUpFirstButtonLabel: "1",
popUpSecondButtonLabel: "2")
popup.modalPresentationStyle = .overCurrentContext
present(popup, animated: animated, completion: completion)
}
}
Is there a way, how to make the first view controller a bit darker with fade animation, when the second appears?
Here you can see what I have now.
You can see an example here, in photos app. When the AlertViewController appears, the background becomes darker. Are there any ideas how to achieve that? Thanks :)
This sounds like you want a Modal Popover.
It is possible to make the background color of the view being placed over another a transparent color. You will need to set the color and everything for that view in it's viewDidLoad.
Ensure that the segue is marked Cover Vertical that so there's no wipe animation, and it's a smooth fade. Also enable either Over Current Context or Over Full Screen depending on how you would like this to look.
The end result will look something like below depending on what elements you place in your second View Controller that you are segueing to. (NOTE: the animation displayed in the gif is sped up. And the dark grey area would contain whatever it is that you place in the view).
There's some great resources out there to help you with making these. I can recommend Mark Moeykens on YouTube, he makes great videos about things like this.

UIPageViewController initial swipe called twice

The below function is how I display different views when a user swipes right where
self.viewControllerAtIndex() is my own custom function that returns a view. The problem is that the first swipe outputs "---------swipe Right before 0" twice. And then works perfectly like expected afterwards.
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
print("---------swipe Right before " + String(index))
index += 1
print("---------swipe Right " + String(index))
if index == (products!.count) {
index = 0
}
return self.viewControllerAtIndex(index:index)
}
=====CONSOLE OUTPUT=====
---------swipe Right before 0
---------swipe Right 1
---------swipe Right before 0
---------swipe Right 1
func viewControllerAtIndex(index: Int) -> ViewController{
return ViewController.init(id: index)
}
For some reason, every other swipe after the first works as expected. The initial swipe is what causes the console output above. This causes my view sequence to look like below (2 views)
First Swipe
View1
Second Swipe
View1
Third Swipe
View2
Fourth Swipe
View1
Fifth Swipe
View2
I'm also initiating my uiPageViewController like so
let array = [ViewController](repeating: ViewController.init(videos: (self.videos![self.products![self.index].id]!)), count: 1)
self.UIViewPageController?.setViewControllers(array, direction: UIPageViewControllerNavigationDirection.forward, animated: false, completion: nil)
So I'm creating a new ViewController on viewDidLoad and then when a user swipes, I'm creating new ViewControllers
I think you should avoid doing index state management in this method. Apple does not guarantee the circumstances under which this method is called. If I had to paraphrase, I would say that while the framework is asking you for the view controller that should come after the provided one, you are answering a different question: Which view controller comes after the one you previously told us about?
To get around this, you need to use your original array of view controllers, find the index of the passed-in view controller in that array, and return the view controller that comes next (or nil if you are at the end of the array).
Too long for a comment ...
Chris Trahey's is the correct answer.
I had the same problem, based on the misunderstanding that the viewControllerBefore/viewControllerAfter methods meant "What should happen if the user swipes right/left". Like you said, these methods only answer the question: "Which view controller comes before/after a given view controller?" They should only answer that question, without any side effects.
A solution for some scenarios would be to add an index property to the view controller class, and set that index on creation. That's useful if you're not working directly with an array of view controllers, but create them on the fly. Then, instead of saying array.index(of: vc), you can say vc.index.
See here: https://stackoverflow.com/questions/25795093/swift-uipageviewcontroller-always-repeats-second-viewcontroller#=
EDIT:
Oh, and if you want to actually react to a page turn, you can do that in pageViewController(_:didFinishAnimating:previousViewControllers:transitionCompleted:).

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

Many view controllers - performance issue

I have an IOS app that lets user swipe through weeks of notes. Each week is a UIViewController - the swiping and switching between the view controllers are handled by a UIPageViewController.
On startup all the view controllers are initialised with their data.
When the user swipes I grap a view controller like this:
func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
if let currentPageViewController = viewController as? SinglePageViewController {
let currentIndex = currentPageViewController.index
return self.weeks[currentIndex - 1]
}
return nil
}
The app work flawless, until a use has many weeks, and thereby many view controllers. Startup time start to become an issue - and this will of cause only get worse as the weeks go on.
I've played around with initialising the each view controller when the user swipes. Like this:
func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
if let currentPageViewController = viewController as? SinglePageViewController {
let currentIndex = currentPageViewController.index
let newVC = SinglePageViewController()
newVC.index = currentIndex - 1
return newVC
}
return nil
}
This approach works and the startup time is great - however, the swiping has now become sluggish and not smooth at all.
Can any one advise on how this issue can be resolved?
The second method (creation on demand) is the correct way to do it. If the swipping gets slow then because you spend to much CPU time in init(), viewDidLoad, viewWillAppear, etc... Look at the initialization and move every CPU intensive task to background threads.
If you depend on data to create the ViewController then you have to preload the data in advance. But it is not needed to preload the data for more then 2 or 3 of them. If it takes to much time and you still run into performane problems then you have to accept that the device is not fast enough for your requirements and you have to present the user an loading indicator. (like UIActivityIndicator)
If you need help in optimizing the initialization then post your code.
I had a similar issue with using too many UIScrollViews inside a UIScrollView. My solution was to monitor where the user was looking, by using the scrollViewDidScroll delegate method, hooked up to my container scrollview, and populate/remove view according to the direction of the user's scroll.
The direction you can get from let direction = scrollView.panGestureRecognizer.translationInView(scrollView.superview!) within the scrollViewDidScroll method.
Would this type of method work for you? I could give you more of my code if you'd like!

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.

Resources