I am just starting to learn how to develop iOS apps. (Note that I am using XCode 6 beta and Swift) I think I'm good with building interfaces themselves, but I seem to be having trouble with segues.
The first page of my app is just a simple screen with a sign up button and a sign in button. I made a separate view controller for a sign up page and put a text field on it. Both of these view controllers are in my storyboard. I then made a custom segue class to simply animate the start page and the sign up page moving one screen to the left, so that the sign up page is now showing. This is my custom segue class:
class SlideFromRightSegue: UIStoryboardSegue {
override func perform() {
let screenWidth = UIScreen.mainScreen().bounds.width
let center = self.sourceViewController.view!.center
UIView.animateWithDuration(0.25,
animations: {
self.sourceViewController.view!.center = CGPoint(x: center.x - screenWidth, y: center.y)
self.destinationViewController.view!.center = CGPoint(x: center.x - screenWidth, y: center.y)
}, completion: { (finished: Bool) in
self.sourceViewController.presentViewController(self.destinationViewController as UIViewController, animated: false, completion: nil)
})
}
}
I based this off a tutorial I found, and I don't think there is a problem here. I simply animate both the source and destination VC's center to be one screen to the left. I'm not entirely sure how animations work in iOS but the tutorial I found was structured similarly, so I assume that the closure passed into the "animations" parameter is called and then iOS generates all of the steps in between and draws each frame to the screen. (correct me if I'm wrong)
So I have a custom segue. I go back to my storyboard, click on the sign up button, go over to the connections tab, and drag the "Triggered Segues - action" over to the second VC, select "custom" and then select my custom class. For some reason, instead of being just "SlideFromRightSegue", it's "_TtC524SlideFromRightSegue". I'm not sure why that's happening, but I assume it has something to do with the fact that I renamed the segue class, and I hope that's not causing any problems. Anyway, doing that creates an arrow from the first VC to the second VC, so I assume it worked.
When I run the app, I get the start screen just like usual. I click the sign up button, and nothing happens. There is probably something I missed, but I put a breakpoint in the perform() function of my segue and another in the prepareForSegue() function of the first VC. Neither one was triggered. I think I have the segue set up properly, I just have something wrong with the actual implementation into my app. Anybody have any idea what's going on?
So yea, it turns out that the problem was that I was using a custom button class as well, which had overrides for the touchesBegan, touchesEnded, and touchesCancelled functions. Those overrides did not callback to the superclass methods, so the action of pressing the button wasn't even being triggered.
I guess the lesson I can take from all this is to make sure to know which function overrides must call back to the superclass, and which should completely override the superclass. Touch event handlers likely need to call back to the superclass.
I don't know why your perform method wasn't called, but it's not due to your code, though the code does have some problems. You never add the destination view controller's view to the window (until the completion where the presentation will do that for you). So, you need to add the view, position it off-screen right, and then have it end up centered, not off screen left like you have in your code,
class SlideFromRightSegue: UIStoryboardSegue {
override func perform() {
let screenWidth = UIScreen.mainScreen().bounds.width
let center = self.sourceViewController.view!.center
let appdel: AppDelegate = UIApplication.sharedApplication().delegate as AppDelegate
appdel.window!.addSubview(self.destinationViewController.view)
self.destinationViewController.view!.center = CGPoint(x: center.x + screenWidth, y: center.y)
UIView.animateWithDuration(0.25,
animations: {
self.sourceViewController.view!.center = CGPoint(x: center.x - screenWidth, y: center.y)
self.destinationViewController.view!.center = center
}, completion: { (finished: Bool) in
self.sourceViewController.presentViewController(self.destinationViewController as UIViewController, animated: false, completion: nil)
self.sourceViewController.view!.removeFromSuperview()
})
}
}
Related
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.
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
I'm having the hardest time implementing a presentation of a drawer sliding partway up on the screen on iPhone.
EDIT: I've discovered that iOS is not respecting the .custom modalTransitionStyle I've set in the Segue. If I set that explicitly in prepareForSegue:, then it calls my delegate to get the UIPresentationController.
I have a custom Segue that is also a UIViewControllerTransitioningDelegate. In the perform() method, I set the destination transitioningDelegate to self:
self.destination.transitioningDelegate = self
and I either call super.perform() (if it’s a Present Modal or Present as Popover Segue), or self.source.present(self.destination, animated: true) (if it’s a Custom Segue, because calling super.perform() throws an exception).
The perform() and animationController(…) methods get called, but never presentationController(forPresented…).
Initially I tried making the Segue in the Storyboard "Present Modally" with my custom Segue class specified, but that kept removing the presenting view controller. I tried "Present as Popover," and I swear it worked once, in that it didn't remove the presenting view controller, but then on subsequent attempts it still did.
So I made it "Custom," and perform() is still being called with a _UIFullscreenPresentationController pre-set on the destination view controller, and my presentationController(forPresented…) method is never called.
Other solutions dealing with this issue always hinge on some mis-written signature for the method. This is mine, verbatim:
public func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController?
I've spent the last four days trying to figure out “proper” custom transitions, and it doesn't help that things don’t seem to behave as advertised. What am I missing?
Instead of using a custom presentation segue, you could use a Container View for your drawer. This way, you can use a UIViewController for your Drawer content, while avoiding the issue with the custom segue.
You achieve this in two steps:
First pull a Container View into your main view controller and layout it properly. The storyboard would look like this: (You can see you have two view controllers. One for the main view and one for the drawer)
Second, you create an action that animates the drawer in and out as needed. One simple example could look like this:
#IBAction func toggleDrawer(_ sender: Any) {
let newHeight: CGFloat
if drawerHeightConstraint.constant > 0 {
newHeight = 0
} else {
newHeight = 200
}
UIView.animate(withDuration: 1) {
self.drawerHeightConstraint.constant = newHeight
self.view.layoutIfNeeded()
}
}
Here, I simply change the height constraint of the drawer, to slide it in and out. Of course you could do something more fancy :)
You can find a demo project here.
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.
Short synopsis (XCode 7.2, Swift2, iOS 9.2 as target):
1) In a first.storyboard, I have a single viewController.
2) In a second.storyboard, I have a tabbarController, with multiple navigationControllers with tableviewControllers (see attached image). Also of note is when second.storyboard is the one used on launch, everything works correctly.
3) the main UI for the app is in the first.storyboard, and I want to present the tabbarcontroller in the second.storyboard
4) No matter which way I present it (storyboard reference/segue, presentViewController, showViewController), the tabbarcontroller and all the initial views work, but if I tap a tableviewcell to segue to another view, the whole tabbarcontroller and contents disappear, leaving me back at the viewcontroller in first.storyboard.
I can cheat, and set the rootViewController manually and things seem to work
let sb = UIStoryboard(name: "second", bundle: nil)
let navController = sb.instantiateViewControllerWithIdentifier("secondIdentifier") as! UITabBarController
UIApplication.sharedApplication().keyWindow?.rootViewController = navController
And I suspect I can add an animation to this to not have the transition not be so stark. But this seems like something I shouldn't have to do, and kind of a pain to troubleshoot in the future. Am I missing something fundamental in making this work?
EDIT: Video of it not working https://youtu.be/MIhR4TVd7CY
NOTE: The last app I made originally targeted iOS4, and I did all the views programatically. It seemed like all the updates to IB and segues etc would make life more manageable (and for the most part that has been true), but this is still my first foray in to it, so I may be missing some important points of information to describe the issue.
I have found a superior way to deal with this: UIViewControllerTransitioningDelegate
It's a bit of extra work to implement, but it produces a "more correct" result.
My solution was to make a custom UIStoryboardSegue that will do the animation as well as set the rootViewController.
import UIKit
class changeRootVCSeguePushUp: UIStoryboardSegue {
override func perform() {
let applicationDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let sourceView = self.sourceViewController.view
let destinationView = self.destinationViewController.view
let sourceFrame = sourceView.frame
let destinationStartFrame = CGRect(x: 0, y: sourceFrame.height, width: sourceFrame.width, height: sourceFrame.height)
let destinationEndFrame = CGRect(x: 0, y: 0, width: sourceFrame.width, height: sourceFrame.height)
destinationView.frame = destinationStartFrame
applicationDelegate.window?.insertSubview(self.destinationViewController.view, aboveSubview: self.sourceViewController.view )
UIView.animateWithDuration(0.25, animations: {
destinationView.frame = destinationEndFrame
}, completion: {(finished: Bool) -> Void in
self.sourceViewController.view.removeFromSuperview()
applicationDelegate.window?.rootViewController = self.destinationViewController
})
}
}
I could not find a way in interface builder, or in code other than changing the rootViewController to get this working. I would end up with various random navigation issue like overlapping navigation bars, segue animations not working correctly until I changed tabs, full on lockups with no information in the console, etc.
I have previously presented a tabBarcontroller modally (without changing rootviewController), but everything was done in code (working as of ios7 and objective-c). No clue what is going on under the covers when the view hierarchies are made in a storyboard, but wondering if this is perhaps a bug.
Thanks to multiple other answers here on stackoverflow to get to mine!