Categories
Past Tutorials Swift WordPress

Swift: View WordPress Posts Natively In iOS

NOTE: Xcode 7 and Swift 2.0 was used for this tutorial
×

In a previous tutorial I covered how to display single WordPress posts in a UIWebView. This particular tutorial is in response to a question I received, asking is it possible to display WordPress posts natively in iOS instead of using a UIWebView. The quick answer is “Of course”, but the real question is, “Do you really want to?”

I’ll be using the files from the Latest Posts project on GitHub. The files have been updated to Swift 2.0. In addition I will be using Version 2 of the WP REST API. I will also take this opportunity to get you up to speed with a few of the new features in Swift 2.0 and WP REST API 2.0

The guard keyword in Swift 2.0

    func populateFields(cell: LatestPostsTableViewCell, index: Int){
        
        //Make sure post title is a string
        guard let title = self.json[index]["title"]["rendered"].string else{
            cell.postTitle!.text = "Loading..."
            return
        }
        
        // An action must always proceed the guard statement
        cell.postTitle!.text = title
        
        //Make sure post date is a string
        guard let date = self.json[index]["date"].string else{
            cell.postDate!.text = "--"
            return
        }
        
        cell.postDate!.text = date
        
        /*
         * Set up Featured Image
         * Using guard, there's no need for nested if statements 
         * to unwrap and check optionals
         */
        
        guard let image = self.json[index]["featured_image_thumbnail_url"].string where
        image != "null"
            else{
            
            print("Image didn't load")
            return
        }
    
        ImageLoader.sharedLoader.imageForUrl(image, completionHandler:{(image: UIImage?, url: String) in
            cell.postImage.image = image!
        })
    }

guard replaces the if keyword and implements the “Bouncer Pattern” to your conditional statements. This is effective in preventing nested conditionals and best suited for instances when you aren’t quite sure about what input is allowed but definitely sure about what input IS NOT allowed. guard is also cleaner with unwrapping optionals, as you see in the example above. This pattern should look familiar to many a WordPress developer.

Alamofire for Network Calls

I’m using Alamofire for networking in this tutorial for a couple of reasons. The first reason is to point out the fact that the JSON results are now handled by enums, and to also cover the parameters parameter that we’ll be leveraging for our WP REST API calls.

func getPosts(getposts : String)
    {
        Alamofire.request(.GET, getposts, parameters:parameters)
            .responseJSON { request, response, result in
                
                switch result
                {
                    case .Success(let data):
                    
                        self.json = JSON(data)
                        self.tableView.reloadData()
                    
                    case .Failure(let error):
                        print("Request failed with error: \(error)")
                }
        }
    }

WARNING: Remote calls will no longer function if you’re server does not support TLS 1.2 and your SSL isn’t SHA2 encrypted. Read About It

The Work Around

Open your Info.plist in text editor. After the last tag, paste the following:

	<key>NSAppTransportSecurity</key>
	<dict>
	    <key>NSExceptionDomains</key>
	    <dict>
	        <key>yourdomain.com</key>
	        <dict>
	            <key>NSExceptionAllowsInsecureHTTPLoads</key>
	            <true/>
	            <key>NSExceptionMinimumTLSVersion</key>
	            <string>TLSv1.1</string>
	            <key>NSIncludesSubdomains</key>
	            <true/>
	        </dict>
	    </dict>
	</dict>

×

Meanwhile, back at the ranch…

If you haven’t already, I suggest you upgrade your WP REST API to version 2, UNLESS you’re using it for production purposes. At the time this tutorial was written, the WP REST API was still in Beta. As you can see, my json url looks a bit different from the previous version:

https://wlcdesigns.com/wp-json/wp/v2/posts/?filter[category_name]=tutorials&filter[posts_per_page]=5

We want to navigate to our functions.php to modify the output of that json call. First we want to get our featured images back, and second, well, we’ll get to that a little later in this tutorial. Add the following to your functions.php:

function my_rest_prepare_post( $data, $post, $request ) {
	
	$_data = $data->data;
	
	//Get featured images back
	$thumbnail_id = get_post_thumbnail_id( $post->ID );
	$thumbnail = wp_get_attachment_image_src( $thumbnail_id, 'full' );
	$_data['featured_image_thumbnail_url'] = $thumbnail[0];
	
	//Remove keys you don't need. 
	unset($_data['author']);
	
	$data->data = $_data;
	return $data;
}

add_filter( 'rest_prepare_post', 'my_rest_prepare_post', 10, 3 );

We’ll revisit this filter later when we build the native single view in our app.

Back to the app…

Navigate back the LatestPostsTableViewController class and find the following constants:

    let latestPosts : String = "https://wlcdesigns.com/wp-json/wp/v2/posts/"
    
    let parameters : [String:AnyObject] = [
        "filter[category_name]" : "tutorials",
        "filter[posts_per_page]" : 5
    ]

Alamofire allows you to better manage your url parameters. Add as many or as few parameters to the parameters dictionary as you need for your network call.

When you fire up the app, you notice nothing has changed client-side. So let’s get started with building out SinglePostViewController. In Xcode, go to File > New > File… and select an iOS Cocoa Touch Class

Next, name the new Cocoa Touch Class “SinglePostViewController” and save.

Navigate to the Main.Storyboard and drag a new View Controller onto the story board. While you’re at it, delete the Web View Controller we created in the previous tutorial if you aren’t working with the updated copy. Select the new View Controller and attach it to the SinglePostViewController class from the Identity Inspector.

Now let’s go back to the SinglePostViewController class and build out interface. Yes, you read correctly, we will build the interface programmatically, because…

I Don’t Like Auto Layout.

I’ve tried patiently to work with Auto Layout, but my patience ran out. If you’re building a native app that doesn’t need to render data from a network call, then use Auto Layout. But if you’re building an app such as the one in this tutorial, where you’re not always sure how much data you will display, then building your interface programmatically is the way to go.  You’ll notice quickly that constraints will break if you have a title that’s longer than the width of the view screen. You’ll also notice inconsistent layouts between different devices. Adding you UI elements programmatically will solve this issues, in addition to boosting performance since the Interface Builder and Auto Layouts adds a little overhead.

UI Elements

Add the following lazy variables inside the SinglePostViewController class, right before the viewDidLoad function:

    lazy var json : JSON = JSON.null
    lazy var scrollView : UIScrollView = UIScrollView()
    lazy var postTitle : UILabel = UILabel()
    lazy var featuredImage : UIImageView = UIImageView()
    lazy var postTime : UILabel = UILabel()
    lazy var postContent : UILabel = UILabel()
    lazy var postContentWeb : UIWebView = UIWebView()
    lazy var generalPadding : CGFloat = 20

Inside the viewDidLoad function, lets’ start by setting up the scrollView:

        //CGRect has 4 parameters: x,y,width,height
        scrollView.frame = CGRectMake(0, 50, self.view.frame.size.width, self.view.frame.size.height)
        
        //We don't need horizontal scrolling
        scrollView.showsHorizontalScrollIndicator = false
        
        //Add the scrollView to the Single Post View Controller
        self.view.addSubview(scrollView)

If you’re coming from a web development background, keep in mind that unlike a browser, scrolling is an optional component in an iOS all. Scroll bars won’t magically appear when you add elements beyond the view port. As a matter of fact, unlike a <div>, elements don’t automatically stack on top of each other either. Always think “position: absolute” when using Swift to add UI elements programmatically. If you launched the app right now and tapped one of the news cells, the app will crash, due to the removal of the Web View Controller. We’ll need to make a few modifications to the LatestPostsTableViewController class to point it to the right direction. In the LatestPostsTableViewController class, comment out or delete the prepareForSegue method and add the following:

override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        
        let singlePostVC : SinglePostViewController = storyboard!.instantiateViewControllerWithIdentifier("SinglePostViewController") as! SinglePostViewController
        singlePostVC.json = self.json[indexPath.row]
        self.navigationController?.pushViewController(singlePostVC, animated: true)
        
    }

From now on, when a cell is selected, it will manually segue to our new Single Post View Controller. But before this can work, we need to go back the Main.storyboard and select the Single Post View Controller. Choose the Identity Inspector from the right panel, and in the Storyboard ID, enter “SinglePostViewController“. Now if you fire up the app and tap a row, you’ll see a blank screen. Don’t worry, our scroll view is there. We still have elements to add.

UILabel: Title

        if let title = json["title"]["rendered"].string{
            
            /*
             * postTitle UI Label position:
             * x = 10px, y = 20px, width = screen width - 20px, height = 1px?!
             */
            
            postTitle.frame = CGRectMake(10, generalPadding, self.view.frame.size.width - 20, 1)
            
            //Title color is black...
            postTitle.textColor = UIColor.blackColor()
            
            //Title alignment is center...
            postTitle.textAlignment = NSTextAlignment.Center
            
            //Break long titles by word wrap
            postTitle.lineBreakMode = NSLineBreakMode.ByWordWrapping
            
            //Font size 24px...
            postTitle.font = UIFont.systemFontOfSize(24.0)
            
            //Number of line 0. Must be set to 0 to accomodate varying title lengths
            postTitle.numberOfLines = 0
            
            //Title text is the json title...
            postTitle.text = title
            
            //This is resizes the height of the title label to accomodate title text. That's why the CGRect height was set to 1px.
            postTitle.sizeToFit()
            
            //Add the postTitle UILabel to the scrollView
            self.scrollView.addSubview(postTitle)
        }

Launch the app and you should now see the title when a news cell is tapped. Continue with the featured image and date:

        if let featured = json["featured_image_thumbnail_url"].string{
            
            /*
            * featuredImage position:
            * x = 10px 
            * y = (height of postTitle + 20px)
            * width = screen width - 20px, 
            * height = 1/3 of screen height. Arbitrary.
            */
            
            featuredImage.frame = CGRect(x: 10, y: postTitle.frame.height + generalPadding, width: self.view.frame.size.width - 20, height: self.view.frame.size.height / 3)
            
            //Fill UIImageView to scale
            featuredImage.contentMode = .ScaleAspectFill
            
            //Equivelant to "overflow: hidden;"
            featuredImage.clipsToBounds = true
            
            //Load image outside main thread
            ImageLoader.sharedLoader.imageForUrl(featured, completionHandler:{(image: UIImage?, url: String) in
                self.featuredImage.image = image!
            })
            
            self.scrollView.addSubview(featuredImage)
        }
        
        if let date = json["date"].string{
            
            postTime.frame = CGRectMake(10, (generalPadding + 10 + postTitle.frame.height + featuredImage.frame.height), self.view.frame.size.width - 20, 10)
            postTime.textColor = UIColor.grayColor()
            postTime.font = UIFont(name: postTime.font.fontName, size: 12)
            postTime.textAlignment = NSTextAlignment.Left
            postTime.text = date
            
            self.scrollView.addSubview(postTime)
        }

And finally the content…

        if let content = json["content"]["rendered"].string{
            
            postContent.frame = CGRectMake(10, (generalPadding * 2 + postTitle.frame.height + featuredImage.frame.height + postTime.frame.height), self.view.frame.size.width - 20, 1)
            postContent.font = UIFont.systemFontOfSize(16.0)
            postContent.numberOfLines = 0
            postContent.text = content
            postContent.lineBreakMode = NSLineBreakMode.ByWordWrapping
            postContent.sizeToFit()
            self.scrollView.addSubview(postContent)

        }

let’s check out progress…

And here is where things get interesting…

So I’m not getting any errors in Xcode’s debugger. There [content][rendered] key is definitely not empty, all the content is there.Obviously something about that particular key is not meshing with Swift. So I decided to tackle the anomaly on the server side first. in the functions.php go back to the my_rest_prepare_post filter. After the featured image block, add:

$_data['content']['rendered'] = strip_shortcodes( $post->post_content );

So I’m using WordPress’s built in strip_shortcodes function to remove all the shortcodes from the json response, if there are any. Now let’s check out the results…

Ok, we still don’t have any content from the Latest Post tutorial, but we do have content on the other views, albeit un-scrollable, un-rendered HTML. At this point I suspect MY ISSUE has something to do with the plugin I’m using to display code snippets. So let’s use something a little strong than the strip_shortcodes function. replace:

$_data['content']['rendered'] = strip_shortcodes( $post->post_content );

with:

$_data['content']['rendered'] = strip_tags( preg_replace("~(?:\[/?)[^/\]]+/?\]~s", '', $post->post_content) );

We stripped out all the tags, shortcodes, and any code snippets than maybe rendered as shortcodes. Before we check our progress, let’s go back into Xcode and and make sure the Single Post View Controller scrolls after the content is added. In the SinglePostViewController class, after the viewDidLoad method, add:

    // MARK: This method fires after all subviews have loaded
    override func viewDidLayoutSubviews() {
        
        //Set variable for final height. Cast it as CGFloat
        var finalHeight : CGFloat = 0
        
        //Loop through all subviews
        self.scrollView.subviews.forEach { (subview) -> () in
            
            //Add each subview height to finalHeight
            finalHeight += subview.frame.height
        }
        
        //Apply final height to scrollview
        self.scrollView.contentSize.height = finalHeight
        
        //NOTE: you maye need to add some padding

    }

There may have been instances in your web development journey where you had to calculate the number of elements and their dimensions on a given page with javascript. The methodology is no different. Now let’s see what we have…

Ok, so we have our WordPress content loaded in a natively in the app, and the scrollView works (don’t forget to add padding at the bottom) but all the formatting, images and context is lost.

This is playing out like a bad 80’s romantic comedy

Swift is that stuffy, constipated retired Army General and his new girlfriend  is WordPress, who is crazy and carefree. The point here is that Swift wants to know exactly what type of input it’s dealing with at all times. In any given post in WordPress you can have code snippets (like my tutorials), video, audio, embedded pdfs, and who know what else a plugin developer will create by the time this tutorial is finished. So how do we marry these 2?

UIWebView

So we’re back here again. But isn’t this cheating? Don’t you have some regex to format all this HTML so we can display it in the UILabel? Where are the HTMLtoSwift libraries?

UIWebView ≠ HTTP Request

In the previous tutorial I used a UIWebView to load a post page. But you don’t have to use the UIWebView to LOAD pages. You can also bake HTML inside of it. Swift doesn’t have dynamically formatted text, nor should it. For more complicated formatting, we are given the UIWebView to work with. I figure if Apple didn’t want ANY web content loaded in their apps they would have never included UIWebView in Xcode. Since we loaded all our JSON upon the initial launch of the app, we don’t need to make another http request.

Comment out the viewDidLayoutSubviews method and the postContent code block in the viewDidLoad method. Navigate to the top of SinglePostViewController class and add the UIWebViewDelegate

class SinglePostViewController: UIViewController, UIWebViewDelegate {

Next, setup the UIWebView in the viewDidLoad method

        if let content = json["content"]["rendered"].string{
            
            postContentWeb.loadHTMLString(content, baseURL: nil)
            postContentWeb.frame = CGRectMake(10, (generalPadding * 2 + postTitle.frame.height + featuredImage.frame.height + postTime.frame.height), self.view.frame.size.width - 20, 1)
            postContentWeb.delegate = self
            self.scrollView.addSubview(postContentWeb)
            
        }

Add after viewDidLoad method

    func webViewDidFinishLoad(webView: UIWebView) {
    
        postContentWeb.frame = CGRectMake(10, (generalPadding * 2 + postTitle.frame.height + featuredImage.frame.height + postTime.frame.height), self.view.frame.size.width - 20, postContentWeb.scrollView.contentSize.height)
        
        var finalHeight : CGFloat = 0
        self.scrollView.subviews.forEach { (subview) -> () in
            finalHeight += subview.frame.height
        }
        
        self.scrollView.contentSize.height = finalHeight
    }

Finally, go back to your functions.php and remove the the strip_tags line:

$_data['content']['rendered'] = strip_tags( preg_replace("~(?:\[/?)[^/\]]+/?\]~s", '', $post->post_content) );

As you can see, the entire page rendered, without making another http request. Depending on the size of your content, there will be a slight delay in rendering. To get around this you can either addSubviews inside the webViewDidFinishLoad method and/or use the webViewDidStartLoad method also provided by the  UIWebViewDelegate to run a cool preloader.

But wait, There’s more!

While we got my page to render natively, retaining the images and code snippets, the formatting is still not quite right because my style sheet didn’t make the trip in the JSON result. No sweat, we’ll just add a style sheet inside the app itself. Go to File > New > File… > iOS > Other > Empty

Name the file appStyles.css (or whatever you want with the extension css) and save. Open the appStyles.css and paste your css inside. Next, Navigate back to the SinglePostViewController class and inside the viewDidLoad method, replace the postContentWeb code block with:

        if let content = json["content"]["rendered"].string{

            let webContent : String = "<!DOCTYPE HTML><html><head><title></title><link rel='stylesheet' href='appStyles.css'></head><body>" + content + "</body></html>"
            let mainbundle = NSBundle.mainBundle().bundlePath
            let bundleURL = NSURL(fileURLWithPath: mainbundle)
            postContentWeb.loadHTMLString(webContent, baseURL: bundleURL)
            postContentWeb.frame = CGRectMake(10, (generalPadding * 2 + postTitle.frame.height + featuredImage.frame.height + postTime.frame.height), self.view.frame.size.width - 20, 1)
            postContentWeb.delegate = self
            self.scrollView.addSubview(postContentWeb)
            
        }

There are still a few crumbs to clean up but you get the picture. So now you know how to natively load content into an iOS app using Swift. But I have a question…

Occam’s razor

Was this tutorial worth avoiding one extra HTTP Request? The previous tutorial also worked just fine. The lesson to be gleaned from this tutorial is that you have more than one way to solve a problem, and that plan your course of action thoroughly before deciding that only one method is the only solution.

NOTE: Don’t forget to add your json url and parameters

    let latestPosts : String = "<-- your wp rest api link here -->"
    
    let parameters : [String:AnyObject] = [
        "filter[category_name]" : "uncategorized",
        "filter[posts_per_page]" : 5
    ]

Also, when you open the project, open it up from the Latest Posts.xcworkspace file.
×

And don’t forget to Git the latest version of this project