Category Archives: Development

Custom modally-presented UIViewController from another UIViewController

As a fast follow-up from the previous Custom UINavigationController Animated Transitions post, let’s say you have a UIViewController up and you want to transition to another UIViewController, but you are not using segues. The simple solution: present.

Create an animator like a fading animation controller

class FadeAnimator: NSObject, UIViewControllerAnimatedTransitioning {
    var duration: TimeInterval = 2.0

    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return duration
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        let containerView = transitionContext.containerView
        let toView = transitionContext.view(forKey: .to)!

        containerView.addSubview(toView)
        toView.alpha = 0

        UIView.animate(withDuration: duration,
                       animations: { toView.alpha = 1.0 },
                       completion: { _ in transitionContext.completeTransition(true) } )
    }
}

Extend your UIViewController as a transition delegate

Easy peasy: just extend the existing class, in this case to do a fade animation.

extension DetailViewController: UIViewControllerTransitioningDelegate {
    func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return FadeAnimator()
    }
}

Finally, instantiate and present!

Ready to show the next UIViewController? Let’s say you want to show a SubdetailViewController:

        let subdetailVC = SubdetailViewController()
        subdetailVC.view.frame = view.frame
        subdetailVC.transitioningDelegate = self
        present(subdetailVC, animated: true, completion: { })

The two bits of magic here are setting your “from” view controller as the transitioning delegate of the new view controller. Then you present with animations. The new controller will be presented modally over the old one.

One more thing: dismissing the new view controller

And when you’re ready to get rid of the new view controller, you can do it from inside it:

    dismiss(animated: true, completion: { })

That’s it!

Custom UINavigationController animated transitions

Thanks to the iOS Animation Tutorial: Custom View Controller Presentation Transitions article combined with A look at UIView Animation Curves (Part 3), it’s not that bad to implement custom transitions on UINavigationControllers. But of course, it’s sprinkling the right ingredients in the right proportions in the right files…

Create an Animator

Let’s start with the animation. If you create a file like SlideAnimator.swift and you put in it:

import UIKit

class SlideAnimator: NSObject, UIViewControllerAnimatedTransitioning {
    var duration: TimeInterval = 1.0

    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return duration
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        let containerView = transitionContext.containerView
        let toView = transitionContext.view(forKey: .to)!

        containerView.addSubview(toView)
        toView.frame = CGRect(x: 0, y: containerView.bounds.height, width: containerView.bounds.width, height: containerView.bounds.height)

        let timingFunction = CAMediaTimingFunction(controlPoints: 0/6, 0.8, 3/6, 1.0)
        CATransaction.begin()
        CATransaction.setAnimationTimingFunction(timingFunction)
        UIView.animate(withDuration: duration, animations: {
            toView.frame = containerView.frame
        }, completion: { _ in
            transitionContext.completeTransition(true)
        } )
        CATransaction.commit()
    }
}

That will take the “to view” that you’re going to and slide it up from the offscreen bottom and overlay the “from view”. In the above code, we do it in 1 second—tune to your liking. In animateTransition there’s a lot of juicy stuff in there:

  • The “to view” needs to be added to the containerView.
  • The transitionContext view needs to be added to the controller’s view.
  • We set the toView offscreen beyond the bottom.
  • We use a Bezier curve timing curve and add that to the CATransaction.
  • We do a usual UIView.animate and position the toView where we eventually want it.
  • The completion block notifies the transition context that it’s done.
  • And then we commit() to begin the animation.

Tell the UINavigationController to use custom transitions

(This does assume you are instantiating UINavigationController programmatically, and that it’s not your root view controller.) In the view controller that hosts your UINavigationController, set its delegate to itself, e.g.

class ViewController: UIViewController {
    var navVC: UINavigationController!
    ...

    override func viewDidLoad() {
        ...
        navVC = UINavigationController(rootViewController: tableVC)
        navVC.delegate = self
        ...
    }
}

Then it’s just a matter of extending that view controller to asking it for an animation controller depending on the operation (push or pop):

extension ViewController: UINavigationControllerDelegate {
    func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        switch operation {
        case .push:
            return SlideAnimator()
        default:
            return nil
        }
    }
}

Believe it or not, that is it!

Pssst. If you want to see how to go from UIViewController to UIViewController without a UINavigationController, you can do custom animations with present().

Creating custom UITableViewCell styles with dequeueReusableCell(withIdentifier:for:)

The trick with Swift always seems getting the right combination of object calls, the right naming, and wading through error messages. I have spent so much time on StackOverflow searching for “custom uitableviewcell” or “custom uitableviewcontroller” and “dequeuereusablecell indexPath fails”, and the Apple docs are as wordy as ever. Trying to use the dequeueReusableCell with the for:indexPath kept crashing my app with error messages like:

Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'unable to dequeue a cell with identifier CustomCell - must register a nib or a class for the identifier or connect a prototype cell in a storyboard'

After much frustration, this is the solution that seems to work the best:

Create a New Table Cell Class

Go ahead and create a new class with a XIB/NIB that inherits from UITableViewCell. You can do this programmatically, but sometimes visually working with autolayout constraints is just easier to grok. Make sure to bind all the labels and images and other UIView things from the NIB to the .swift class.

Create a New Table Controller

Create a new class inheriting from UITableViewController but no need to create a NIB unless you need. In the cellForRowAt method, you’ll want to dequeue as your custom table cell.

    let REUSE_IDENTIFIER = "YourCustomID"

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: REUSE_IDENTIFIER, for: indexPath) as! TableViewCell

        // Configure the cell...

        return cell
    }

Register the cell NIB on the tableView

Finally, the part that just killed me for the two days was how/when/where to register the NIB. And, the easiest is in the viewDidLoad of the table view controller:

    override func viewDidLoad() {
        super.viewDidLoad()

        tableView.register(UINib.init(nibName: "TableViewCell", bundle: nil), forCellReuseIdentifier: REUSE_IDENTIFIER)
    }

Notice that the register is on the tableView.

And that, is how you do it.

Reading JSON from a file (in the Documents folder) into an NSDictionary in iOS

So you have a JSON file stored in the user’s Documents directory in your iOS app, and now you want to read it. First, you get the URL to the file by getting the user’s NSDocumentDirectory, then you can append your filename to it. To get the data out, you can read with NSString and use NSJSONSerialization JSONObjectWithData:options:error:. Note that in the Gist NSJSONReadingMutableContainers is used to ensure the dictionary is an NSMutableDictionary instead of NSDictionary. (You can choose other options to make the dictionary immutable, or partially mutable.)

PhoneGap: automated app building with Grunt, Node, Watch, Sass

After much fiddling around, this is the solution I’ve ended up with that uses Grunt, Node, Watch, and Sass:

  • Install Node: http://nodejs.org
  • Create a package.json: NOTE: I froze versions of my dependencies. You may want to use ~ instead.
  • Install/update node packages: npm install
  • Create a Sass directory in your www: mkdir www/sass
  • Add an SCSS file in your www/sass
  • Add a Grunt config script:
  • Run Grunt (and it will keep rebuilding any time a change to app.scss or your www/* files happens): grunt
  • Open the Xcode project: open platforms/ios/YourAppProjectName.xcodeproj/
  • Run the iOS simulator

PhoneGap: why there is no platform.js

There is no platform.js in your PhoneGap project anywhere because it’s autogenerated. However, if you are getting errors that platform.js never gets generated, perhaps you’re not running the right build command. There is a difference! cordova build ios is not correct with PhoneGap. Rather use:

# *Not* cordova!
phonegap build ios

PhoneGap: removing the iOS 7 status bar

A quick guide removing the iOS 7 status bar:

  • Visit https://github.com/phonegap-build/StatusBarPlugin
  • Add the plugin: cordova add plugin https://github.com/phonegap-build/StatusBarPlugin.git
  • Add this to your config.xml:
    <gap:plugin name="com.phonegap.plugin.statusbar" />
    <gap:config-file platform="ios" parent="UIStatusBarHidden">
        <true/>
    </gap:config-file>
    <gap:config-file platform="ios" parent="UIViewControllerBasedStatusBarAppearance">
        <false/>
    </gap:config-file>
    <preference name="StatusBarOverlaysWebView" value="true" />
  • Rebuild: phonegap build ios

PhoneGap: changing the iOS app name

A quick guide on changing the iOS app name:

  • Remove the iOS platform first: cordova platform remove ios
  • Edit www/config.xml:
  • Change the <widget> id, name, description, and author entries to your liking
  • Add the iOS platform back: cordova platform add ios