Parallax UITableView Header and Keeping it DRY

Last night I was working on a UITableViewController of a native iOS project. It was all standard stuff, so I feel like adding some eye candy to the view.

Many apps now add a Parallax Effect to their table view header, making the UI more lively and vivid. There are libs out there that do this for you, but then where is the fun in that.

So I went to search for a tutorial, there are some good ones, but I end up using this one from Matthew Cheok. Below is the final product of his tutorial.

alt text

The good things about Matthew’s tutorial is that he uses story board, has video instructions, and utilizes many things like struct and UIBezierPath.

When I applied Matthew’s technique into my project, I had a problem. I want apply this parallax effect to all my table views that has a header background image. As a good software developer, we should always keep our code DRY. So I did it with a some very simple setup.

Create ParallaxTableView class

First we will create a new UITableView subclass containing all the code needed to construct and update a parallax header:

import UIKit

class ParallaxTableView: UITableView {

    // these are public so we can change them in controller when needed
    var kTableHeaderHeight: CGFloat = 300.0
    var kTableHeaderCutAway: CGFloat = 80.0
    var kOverlapRatio: CGFloat = 3

    private var headerView: UIView?
    private var headerMaskLayer: CAShapeLayer?

    func constructParallaxHeader() {
        if self.tableHeaderView !== nil {
            // get the original table header
            headerView = self.tableHeaderView
            // remove the header from table view
            self.tableHeaderView = nil

            // add the header back to table view as a subview
            self.addSubview(headerView!)
            let effectiveHeight = kTableHeaderHeight - kTableHeaderCutAway / kOverlapRatio
            self.contentInset = UIEdgeInsets(top: effectiveHeight, left: 0, bottom: 0, right: 0)
            self.contentOffset = CGPoint(x: 0, y: -effectiveHeight)

            // construct cut away
            headerMaskLayer = CAShapeLayer()
            headerMaskLayer!.fillColor = UIColor.black.cgColor
            headerView!.layer.mask = headerMaskLayer
            // call the update to calculate header size and cut away
            updateHeaderView()
        }
    }

    func updateHeaderView() {
        let effectiveHeight = kTableHeaderHeight - kTableHeaderCutAway / kOverlapRatio
        var headerRect = CGRect(x: 0, y: -effectiveHeight, width: self.bounds.width, height: kTableHeaderHeight)
        if self.contentOffset.y < -effectiveHeight {
            headerRect.origin.y = self.contentOffset.y
            headerRect.size.height = -self.contentOffset.y + kTableHeaderCutAway / kOverlapRatio
        }

        let path = UIBezierPath()
        path.move(to: CGPoint(x: 0, y: 0))
        path.addLine(to: CGPoint(x: headerRect.width, y: 0))
        path.addLine(to: CGPoint(x: headerRect.width, y: headerRect.height))
        path.addLine(to: CGPoint(x: 0, y: headerRect.height - kTableHeaderCutAway))
        headerMaskLayer?.path = path.cgPath

        headerView?.frame = headerRect
    }

}

Most of the code is identical to Matthew’s tutorial, which you should definitely take a look if you have any trouble understanding what each line does.

Change the class of UITableView in storyboard

For any UITableView that you want to give it a parallax header, change its class to the ParallaxTableView class you just created.

Connect a IBOutlet from ParallaxTableView to its controller

We need an IBOutlet to the ParallaxTableView so we can call the construct and update functions when the table view is loaded. We do this by Ctrl-Drag, and I will name it parallaxTableView.

Call construct and update function in UITableViewController

Finally, we need to call the ParallaxTableView‘s function in the controller.

class ItemDetailTableViewController: UITableViewController {
    @IBOutlet var parallaxTableView: ParallaxTableView!

    ...

    override func viewDidLoad() {
        super.viewDidLoad()

        ...

        // call the construct function in viewDidLoad()
        parallaxTableView.constructParallaxHeader()

        ...
    }

    ...

    // MARK: - Scroll view delegate

    override func scrollViewDidScroll(_ scrollView: UIScrollView) {
        // call the update function in scrollViewDidScroll()
        parallaxTableView.updateHeaderView()
    }

    ...
}

Done

If you build and run your project now. Your UITableView should have a parallax header. If you want to add this to any other table view in your project, just change the class, add the outlet, and call 2 functions in its controller!

As you can see I have also made the configuration variables in ParallaxTableView to be public, so if you want a shorter parallax header, you simply do:

    parallaxtableView.kTableHeaderHeight = 100.0
    parallaxtableView.constructParallaxHeader()

Then your header will be 100 pixels tall.

React Native URL Scheme and Linking API

React Native is possible the best hybrid framework for building mobile app at the moment. Not just its performance but also the development experience it provides. If you build it well, one really can’t tell if your app is native or a hybrid.

I spent my last 3 month developing a complicated hybrid app using react native. It involves many network requests, forms, lists, etc. It was undoubtedly challenging, but the result was quite satisfying.

For my next project, I was given another chance to work with React Native. This time I need to build a “AppStore” kind of application, which allow the user to install apps and launch installed apps right from this custom “AppStore”.

To launch other Apps in iOS or Android, we must register custom URL Schemes in the Apps we wish to launch.

Register URL Schemes

iOS

On iOS, we need to add a key CFBundleURLTYpes to the Info.plist file within the Xcode project.

Under this key we add an array, and define 2 keys CFBundleURLName and CFBundleURLSchemes.

Under CFBundleURLName you need to add a unique string that identifies your bundle, I used the same string as my app’s bundle identifier and it worked fine. Under CFBundleURLSchemes you need to add another array, and put in your desired URL schemes.

Here is an example of such entry inside Info.plist:

<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleURLName</key>
        <string>com.johnsonsu.example</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>johnsonsu-example</string>
        </array>
    </dict>
</array>

More info about iOS URL Scheme can be found on the official documentation.

Android

Android’s setup is similar to iOS. We need to add a custom intent-filter in the Android app’s AndroidManifest.xml file.

Here is an example of such intent-filter:

<intent-filter>
    <action android:name="android.intent.action.MAIN" />
    <category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT"/>
    <category android:name="android.intent.category.BROWSABLE"/>
    <data android:scheme="johnsonsu-example"/>
</intent-filter>

Since React Native’s Linking API will be creating an intent with action equals to android.intent.action.VIEW, your intent-filter must contain an action tag with this name.

The data tag is where you should define your custom URL Scheme. In this example I used the same one as the iOS scheme. Doing it this way allows me to use the exact same code in React Native to launch these applications in both iOS and Android.

More info about Android Intent Filter can be found on the official documentation.

Launching the App in React Native

If you setup your apps properly, you should be able to launch that app in React Native using:

import { Linking } from 'react-native';

Linking.openURL('johnsonsu-example://').catch(err => console.error('An error occurred', err));

If the OS can’t handle the URL you tried to open, the exception will be caught inside the catch block, and you should handle it properly to prevent a crash.

A safer way to open URL is to check if the OS can handle it before you open it. You can check it using:

import { Linking } from 'react-native';

const url = 'johnsonsu-example://';
Linking.canOpenURL(url).then(supported => {
  if (!supported) {
    console.log('Can\'t handle url: ' + url);
  } else {
    return Linking.openURL(url);
  }
}).catch(err => console.error('An error occurred', err));

This works on Android without any setup. However, on iOS 9 or above, you need to add LSApplicationQueriesSchemes to your React Native app’s Info.plist or Linking.canOpenURL(url) will always returns false.

This is an example entry in Info.plist:

<key>LSApplicationQueriesSchemes</key>
<array>
    <string>johnsonsu-example</string>
    <string>youtube</string>
    <string>facebook</string>
</array>

There you have it, a simple way to launch any other apps from your React Native app!

How to move WordPress to another Server

My friend’s 1 year free tier from AWS just expired. Instead of paying for the subscription, she ask if she can move her WordPress blog to my EC2 server.

I don’t get a lot of traffics on my server, thus hosting another WordPress should be fine.

Backing up WordPress

WordPress is basically a bunch of files and a database. Backing up all the files and database, transferring them to the new server, then we should be good.

Backup Database

WordPress recommend using phpMyAdmin to do this. But I decided to use mysqldump, which should be installed in Ubuntu LAMP server already.

# mysqldump Usage
mysqldump [OPTIONS] database [tables]
mysqldump [OPTIONS] --databases [OPTIONS] DB1 [DB2 DB3...]
mysqldump [OPTIONS] --all-databases [OPTIONS]

As you can see, you need to know your WordPress database name to back it up. My database name is just wordpress, so I ssh to my server and ran:

mysqldump wordpress > wordpress.sql

This creates a wordpress.sql file in the current working directory.

Backup WordPress Files

I installed my WordPress on /var/www/wordpress, so this is the folder I need to backup.

# Mind the dot at the end
tar -zcvf wordpress-backup.tar.gz -C /var/www/wordpress .

This will create a wordpress-backup.tar.gz file that contains all the WordPress file in the current working directory.

Download the Backups

To download the backup files, we need to use SFTP to connect to the EC2 instance. I used Cyberduck, it’s free and easy to use.

Since EC2 uses public key authentication, we need to check the Use Public Key Authentication checkbox at the bottom, then select your instance’s .pem file. Username should be ubuntu if you setup your EC2 using the Ubuntu 14.04 image.

cyberduck

After you connect, simply locate the wordpress.sql and wordpress.tar.gz file and download them to your computer.

Moving to New Server

Using Cyberduck and SFTP, connect to your new server and upload the 2 backup files. The procedure is very similar to how we download the backup files. Just change the server address and .pem file.

Then ssh to your new server.

Restore the Database

We will use mysql to restore the database, but before we do that, we need to create a new empty database.

Login to mysql using:

mysql -u root -p

You will be asked for your MySQL database password.

In the mysql command prompt, we then run:

# you should choose a better database name than wordpress2
CREATE DATABASE wordpress2;

This will be where we save the restored database.
Then we need to create a new user for the restored WordPress, so it can access this database.

# create a new user
CREATE USER [email protected] identified by 'password';

# give the new user access to wordpress2 database
GRANT ALL PRIVILEGES ON wordpress2.* TO [email protected];

# make sure the changes take effect
FLUSH PRIVILEGES;

# exit mysql prompt
exit;

Be sure to match your database name with the one you created in the previous step, and choose a better password.

Now we can restore the database using:

mysql wordpress2 < wordpress.sql

Restore the Files

First, uncompress the wordpress.tar.gz file we uploaded using:

tar -xvzf community_images.tar.gz

This will create the same folder in the current working directory as the one you compressed previously.

If this folder is not in the /var/www/ directory, use the mv command to move it there.

You should also run:

sudo chown -R www-data:www-data /var/www

In case the uncompressed folder has a different ownership.

Add a Virtual Host

Depends how your server is setup, you need to update your apache or Ngnix so it recognizes the newly added WordPress blog. I’m using apache, and this is what I did:

# cd to the vhost config directory
cd /etc/apache2/sites-available

# copy my wordpress's config file for the new wordpress
cp my-wordpress.conf new-wordpress.conf

# edit the new-wordpress.conf
vim new-wordpress.conf

I modified 4 rows in new-wordpress.conf:

// new-wordpress.conf

<VirtualHost *:80>
    ServerAdmin [email protected]
    ServerName domain-of-my-friends-wordpress.com
    ServerAlias www.domain-of-my-friends-wordpress.com
    DocumentRoot /var/www/wordpress2
    ...

Then reload apache:

sudo service apache2 reload

Change DNS of the domain

You now need to update your DNS with the IP address of the new server. Depends on which domain registrar, you will use different tools. Simply updating the IP address value in the A record to the IP address of the new server should do the job.

See the Result

Wait for your DNS to update, then enter the domain in your browser. You should see the same WordPress site like nothing have changed!