WordPress has been around for almost a decade and a half. It survived the CMS wars and remained relevant during the mobile shift. It’s been battle-tested under various scenarios and load. It showed us what a thriving 3rd party marketplace looks like. Now, WordPress is realizing a grander vision!
In the release of WordPress 4.7, the REST API plugin was merged into core and enabled by default. This means you can request the data format of any WordPress 4.7+ site out there. Go ahead, try this very blog out for yourself: http://zamzam.io/wp-json/wp/v2/posts:

It’s what the next web looks like – data! WordPress is trying to live beyond the browser, where the Internet is being taken over by IoT, AI, microservices, {enter latest geek buzzword here}, etc. In this post, I’d like to show how we can leverage WordPress and use it as a solid backend for our native Swift app.
The Project Plan
It usually helps to work backwards from a goal, so below is the app we will end up with. It’s actually the live app for this blog and is fully open source:

First, a few ground rules before we start building this WordPress app:
- Offline: The app has to work with no internet connection (that’s a major advantage over a website!).
- Updates: The content should be fresh and up-to-date on the app.
- Reusable: Being able to package this as an app framework and easily used for other WordPress sites.
The last one might be too ambitious, but I’ve already done it! See this app which uses the same framework, but a different WordPress site and “theme”.

Ok, now that you know the plan:

The Framework
As part of our goals, this has to be a framework so we can reuse it. In my previous post, called “Creating Cross-Platform Swift Frameworks“, I cover how we can setup a fresh Xcode workspace for a framework so it can be distributed across projects. The framework structure will look like this:

Now we have a place to code!
The Configuration
Before we go much further, we need a simple and manageable way to store configuration values. I cover how we can read values from a plist file in my previous post. In the end, our plist configuration file will look something like this:

This way, apps consuming the framework only have to set a few configuration values in a plist file to wire it to their WordPress site, as well as add some look and feel changes. This will get read into memory when the app starts up:
class AppDelegate: UIResponder, UIApplicationDelegate, AppPressable { var window: UIWindow? override init() { super.init() UserDefaults.standard.registerDefaults("Settings.plist") } ... }
This is extending Apple’s native “UserDefaults” function called “register(defaults:)” to store a dictionary into the user defaults. In this case, it’s first converting the plist into a dictionary, then using the underlying native “register” call to store the values into user defaults for later use.
Ground Zero: The Database
An application is only as good as its data. That’s why it’s important to think of what the data schema will look like. Seeing what the WordPress JSON looks like out of the box, things can get messy and confusing. Instead, I’d like to extend the new WordPress REST API to provide my own slim, simplified endpoint:

Fortunately, the new REST API is super flexible and well thought out to do this. Below is the WordPress extension point to make this happen, or you can install my WordPress SwiftyPress plugin to get it done (ya, it’s in PHP):
<?php class SwiftyPress_REST_Post_Controller { // Here initialize our namespace and resource name. public function __construct() { $this->namespace = '/swiftypress/v2'; $this->resource_name = 'posts'; $this->date_format = 'Y-m-d\TH:i:s'; } // Register our routes. public function register_routes() { register_rest_route($this->namespace, '/' . $this->resource_name, array( array( 'methods' => 'GET', 'callback' => array($this, 'get_items') ), 'schema' => array($this, 'get_item_schema') )); register_rest_route($this->namespace, '/' . $this->resource_name . '/(?P<id>[\d]+)', array( array( 'methods' => 'GET', 'callback' => array($this, 'get_item') ), 'schema' => array($this, 'get_item_schema') )); } /** * Grabs the five most recent posts and outputs them as a rest response. * * @param WP_REST_Request $request Current request. */ public function get_items($request) { // Construct query options $params = array(); if (isset($request['per_page'])) { $per_page = (int)$request['per_page']; if ($per_page > 0) { $params['posts_per_page'] = $per_page; if (isset($request['page'])) { $page_nbr = (int)$request['page']; $params['offset'] = $page_nbr * $per_page; } } } if (isset($request['orderby'])) { $params['orderby'] = $request['orderby']; } if (isset($request['order'])) { $params['order'] = strtoupper($request['order']); } $posts = get_posts($params); $data = array(); if (empty($posts)) { return rest_ensure_response($data); } foreach ($posts as $post) { $response = $this->prepare_item_for_response($post, $request); $data[] = $this->prepare_response_for_collection($response); } // Return all response data. return rest_ensure_response($data); } /** * Grabs the five most recent posts and outputs them as a rest response. * * @param WP_REST_Request $request Current request. */ public function get_item($request) { $id = (int)$request['id']; $post = get_post($id); if (empty($post)) { return rest_ensure_response(array()); } $response = $this->prepare_item_for_response($post, $request); // Return all response data. return $response; } /** * Matches the post data to the schema we want. * * @param WP_Post $post The comment object whose response is being prepared. */ public function prepare_item_for_response($post, $request) { $post_data = array(); $schema = $this->get_item_schema($request); // We are also renaming the fields to more understandable names. if (isset($schema['properties']['id'])) { $post_data['id'] = (int)$post->ID; } if (isset($schema['properties']['title'])) { $post_data['title'] = $post->post_title; } if (isset($schema['properties']['slug'])) { $post_data['slug'] = $post->post_name; } if (isset($schema['properties']['type'])) { $post_data['type'] = $post->post_type; } if (isset($schema['properties']['excerpt'])) { $post_data['excerpt'] = $post->post_excerpt; } if (isset($schema['properties']['date'])) { $post_data['date'] = get_the_date($this->date_format, $post->ID); } if (isset($schema['properties']['modified'])) { $post_data['modified'] = get_post_modified_time($this->date_format, null, $post->ID); } if (isset($schema['properties']['comment_count'])) { $post_data['comment_count'] = (int)$post->comment_count; } if (isset($schema['properties']['link'])) { $post_data['link'] = get_permalink($post->ID); } if (isset($schema['properties']['author'])) { $post_data['author'] = array( 'id' => (int)$post->post_author, 'username' => get_the_author_meta('user_login', $post->post_author), 'email' => get_the_author_meta('user_email', $post->post_author), 'name' => get_the_author_meta('display_name', $post->post_author), 'link' => get_the_author_meta('url', $post->post_author), 'avatar' => get_avatar_url($post->post_author), 'description' => get_the_author_meta('description', $post->post_author) ); } if (isset($schema['properties']['featured_media'])) { $attachment_id = get_post_thumbnail_id($post->ID); if ($attachment_id > 0) { $full = wp_get_attachment_image_src($attachment_id, 'full'); $thumbnail = wp_get_attachment_image_src($attachment_id, 'medium'); $post_data['featured_media'] = array( 'link' => $full[0], 'width' => (int)$full[1], 'height' => (int)$full[2], 'thumbnail_link' => $thumbnail[0], 'thumbnail_width' => (int)$thumbnail[1], 'thumbnail_height' => (int)$thumbnail[2] ); } else { $post_data['featured_media'] = null; } } if (isset($schema['properties']['categories'])) { $terms = get_the_category($post->ID); if (!empty($terms)) { $post_data['categories'] = array_map( function($item) { return array( 'id' => (int)$item->term_id, 'parent' => (int)$item->parent, 'name' => $item->name, 'slug' => $item->slug, 'taxonomy' => $item->taxonomy ); }, $terms ); } else { $post_data['categories'] = []; } } if (isset($schema['properties']['tags'])) { $terms = get_the_tags($post->ID); if (!empty($terms)) { $post_data['tags'] = array_map( function($item) { return array( 'id' => (int)$item->term_id, 'parent' => (int)$item->parent, 'name' => $item->name, 'slug' => $item->slug, 'taxonomy' => $item->taxonomy ); }, $terms ); } else { $post_data['tags'] = []; } } if (isset($schema['properties']['content'])) { $post_data['content'] = apply_filters('the_content', $post->post_content, $post); } return rest_ensure_response($post_data); } /** * Prepare a response for inserting into a collection of responses. * * This is copied from WP_REST_Controller class in the WP REST API v2 plugin. * * @param WP_REST_Response $response Response object. * @return array Response data, ready for insertion into collection data. */ public function prepare_response_for_collection($response) { if (!($response instanceof WP_REST_Response)) { return $response; } $data = (array)$response->get_data(); $server = rest_get_server(); if (method_exists($server, 'get_compact_response_links')) { $links = call_user_func(array($server, 'get_compact_response_links'), $response); } else { $links = call_user_func(array($server, 'get_response_links'), $response); } if (!empty($links)) { $data['_links'] = $links; } return $data; } /** * Get our sample schema for a post. * * @param WP_REST_Request $request Current request. */ public function get_item_schema($request) { $schema = array( // This tells the spec of JSON Schema we are using which is draft 4. '$schema' => 'http://json-schema.org/draft-04/schema#', // The title property marks the identity of the resource. 'title' => 'post', 'type' => 'object', // In JSON Schema you can specify object properties in the properties attribute. 'properties' => array( 'id' => array( 'description' => esc_html__('Unique identifier for the object.', 'my-textdomain'), 'type' => 'integer', 'context' => array('view', 'edit', 'embed'), 'readonly' => true ), 'title' => array( 'description' => esc_html__('The title for the object.', 'my-textdomain'), 'type' => 'string' ), 'slug' => array( 'description' => esc_html__('The slug for the object.', 'my-textdomain'), 'type' => 'string' ), 'type' => array( 'description' => esc_html__('The type for the object.', 'my-textdomain'), 'type' => 'string' ), 'excerpt' => array( 'description' => esc_html__('The excerpt for the object.', 'my-textdomain'), 'type' => 'string' ), 'date' => array( 'description' => esc_html__('The date for the object.', 'my-textdomain'), 'type' => 'string' ), 'modified' => array( 'description' => esc_html__('The modified for the object.', 'my-textdomain'), 'type' => 'string' ), 'comment_count' => array( 'description' => esc_html__('The comment count for the object.', 'my-textdomain'), 'type' => 'integer' ), 'link' => array( 'description' => esc_html__('The link for the object.', 'my-textdomain'), 'type' => 'string' ), 'author' => array( 'description' => esc_html__('The author for the object.', 'my-textdomain'), 'type' => 'object' ), 'featured_media' => array( 'description' => esc_html__('The featured media for the object.', 'my-textdomain'), 'type' => 'object' ), 'content' => array( 'description' => esc_html__('The content for the object.', 'my-textdomain'), 'type' => 'string' ), 'categories' => array( 'description' => esc_html__('The categories for the object.', 'my-textdomain'), 'type' => 'object' ), 'tags' => array( 'description' => esc_html__('The tags for the object.', 'my-textdomain'), 'type' => 'object' ) ) ); return $schema; } // Sets up the proper HTTP status code for authorization. public function authorization_status_code() { return is_user_logged_in() ? 403 : 401; } }
It’s mostly boilerplate code, but now I get my own route with endpoints and schemas. Here is what we’re working with now: http://zamzam.io/wp-json/swiftypress/v2/posts. The WordPress documentation provides some more details and samples in creating your own custom endpoints.
Designing the app as offline-first is important so we don’t jam ourselves into a corner later. So we need a way to persist this data locally in the app to ensure it works offline; for this we will use Realm. Here’s how the database will look like in the app:

The Models
We have our database and the endpoints to update it, now we can start designing our Swift models. Let’s first start with the “Post” model:
public protocol Postable: class { var id: Int { get set } var title: String { get set } var content: String { get set } var excerpt: String { get set } var slug: String { get set } var type: String { get set } var link: String { get set } var date: Date? { get set } var modified: Date? { get set } var commentCount: Int { get set } var media: Media? { get set } var author: User? { get set } var categories: List<Term> { get } var tags: List<Term> { get } } public class Post: Object, Postable { public dynamic var id = 0 public dynamic var title = "" public dynamic var content = "" public dynamic var excerpt = "" public dynamic var slug = "" public dynamic var type = "" public dynamic var link = "" public dynamic var date: Date? public dynamic var modified: Date? public dynamic var commentCount = 0 public dynamic var media: Media? public dynamic var author: User? public var categories = List<Term>() public var tags = List<Term>() public override static func primaryKey() -> String? { return "id" } public override static func indexedProperties() -> [String] { return [ "title", "slug", "date" ] } public convenience init(json: JSON) { self.init() id = json[.id] title = json[.title] content = json[.content] excerpt = json[.excerpt] slug = json[.slug] type = json[.type] link = json[.link] date = json[.date] modified = json[.modified] commentCount = json[.commentCount] // Retrieve associated models author = User(json: json[.author]) categories = List<Term>(json[.categories].map(Term.init)) tags = List<Term>(json[.tags].map(Term.init)) let image = Media(json: json[.media]) if !image.link.isEmpty { media = image } } }
Notice my “convenient init“, where I can instantiate the model from JSON. I’m using a Swifty library called JASON, but you can choose another library or manually do the conversion.
You can see the source code for the other models, but basically I created models for: Media (images), Post, Term (categories and tags), and User (author).
The Service
We need a way to query the data from the local Realm and remote WordPress databases. We can create a centralized service per model to do this, such as “PostService“:
public struct PostService { public func get(complete: @escaping ([Post]) -> Void) { guard let realm = Realm(), let list = realm.objects(Post.self) else { return [] } complete(list) } func getFromRemote(id: Int, complete: @escaping (Post) -> Void) { Alamofire.request(PostRouter.readPost(id)) .responseJASON { response in guard let json = response.result.value, response.result.isSuccess else { return } complete(Post(json: json)) } } }
The first “get” function retrieves the posts from the local Realm database. We will also need the “getFromRemote” function to retrieve data from the cloud (WordPress), which passes in the remote JSON to the model’s convenient init to be saved into Realm later.
To make things maintainable and more elegant, I created a “PostRouter” that houses all our REST API calls via Alamofire’s nifty “URLRequestConvertible” protocol. This way, the REST API calls can be changed and managed in one place:
enum PostRouter: URLRequestConvertible { case readPost(Int) case readPosts(Int, Int, String, Bool) case commentCount(Int) case commentsCount static let baseURLString = "http://zamzam.io" static let baseRESTString = "wp-json/swiftypress/v2" var method: HTTPMethod { switch self { case .readPost: return .get case .readPosts: return .get case .commentCount: return .get case .commentsCount: return .get } } var path: String { switch self { case .readPost(let id): return "/posts/\(id)" case .readPosts(_, _, _, _): return "/posts" case .commentCount(let id): return ("/comments/\(id)/count") case .commentsCount: return "/comments/count" } } func asURLRequest() throws -> URLRequest { let url = try PostRouter.baseURLString.asURL() var urlRequest = URLRequest(url: url .appendingPathComponent(PostRouter.baseRESTString) .appendingPathComponent(path)) urlRequest.httpMethod = method.rawValue switch self { case .readPosts(let page, let perPage, let orderBy, let ascending): urlRequest = try URLEncoding.default.encode(urlRequest, with: [ "page": page, "per_page": perPage, "orderby": orderBy, "order": ascending ? "asc" : "desc" ]) case .commentsCount, .commentCount(_): urlRequest = try URLEncoding.default.encode(urlRequest, with: [ "cache": Date().timeIntervalSince1970 as Any ]) default: break } return urlRequest } }
This allows me to query the REST API services without knowing the URL or parameters all the time. We’re ready to connect the pipes to seed the database.
App Lift Off!
In the launch of the app, we will seed the fresh database with posts saved from the server. In “AppDelegate.didFinishLaunchingWithOptions” we will check if the database exists. The first time it will need to be created and seeded with data.
To do this, we will need a function on our “PostService” to update the local data from the remote server. A good way to do this is to query the posts sorted by descending modified date order, then merge them into Realm if the local modified date is older than the server or doesn’t exist at all:
func updateFromRemote(page: Int = 0, perPage: Int = 50, orderBy: String = "post_modified", ascending: Bool = false, complete: ((Result<Void>) -> Void)? = nil) { Alamofire.request(PostRouter.readPosts(page, perPage, orderBy, false)) .responseJASON { response in guard response.result.isSuccess, let realm = try? Realm(), let json = response.result.value, !json.arrayValue.isEmpty else { complete?(.failure(response.result.error ?? PressError.emptyPosts)) return } // Parse JSON to array let list: [Post] = json.map(Post.init).filter { // Skip if latest changes already persisted if let persisted = AppGlobal.realm?.object(ofType: Post.self, forPrimaryKey: $0.id), let localDate = persisted.modified, let remoteDate = $0.modified, localDate >= remoteDate { return false } return true } if !list.isEmpty { do { try realm.write { realm.add(List(list), update: true) } } catch { // TODO: Log error } } complete?(.success()) } }
It retrieves the JSON from WordPress, compares the modified property to the local post model in Realm, then persists it if it is older or doesn’t exist.
This is great for updating the last few posts, but seeding the database from scratch will need to be handled different since there can be hundreds or even thousands of posts. We can do this using a recursive function that persists posts via pagination:
func seedFromRemote(for page: Int = 0, complete: (() -> Void)? = nil) { updateFromRemote(page: page, orderBy: "post_date") { guard $0.isSuccess else { complete?(); return } self.seedFromRemote(for: page + 1, complete: complete) } }
This function calls the previous “updateFromRemote” function several times until there are no more pages left. Run this function called “PostService.seedFromRemote” in the “AppDelegate.didFinishLaunchingWithOptions” so the recursive scraping begins!
Optimizing the App Startup
We can achieve a huge performance boost by running the app in our development environment and get the completed database from the app to use as a seed database for new app installs. This is much more performant to copy the seed database instead of querying the data page-by-page and parsing it. Here’s the function you can use to copy the seed database to the user’s Realm space:
func setupDatabase() { let fileManager = FileManager.default // Skip if database already exists guard let realmFileURL = Realm.Configuration.defaultConfiguration.fileURL, !fileManager.fileExists(atPath: realmFileURL.path) else { return } // Seed data to fresh database guard let seedFileURL = Bundle.main.url(forResource: "seed", withExtension: "realm", subdirectory: "\(AppGlobal.userDefaults[.baseDirectory])/data"), fileManager.fileExists(atPath: seedFileURL.path) else { // Construct from a series of REST requests return PostService().seedFromRemote() } // Use pre-created seed database do { try fileManager.copyItem(at: seedFileURL, to: realmFileURL) } catch { /*TODO: Log error*/ } }
It checks if the Realm database exists. If it doesn’t, it will look for the seed database to copy to the user’s Realm database path. It will fallback to our recursive function that will seed the database from cloud via REST API and pagination if the seed database doesn’t exist.
The seeding of the database should also be running in the “AppDelegate“. However, it must be run on first installs only or the database will be overwritten every time. I’m using a neat library called UpdateKit to know when the app is freshly installed or even when the app updates:
class AppDelegate: UIResponder, UIApplicationDelegate func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { UpdateKit().firstLaunch { setupDatabase() } } }
Keeping Posts Updated
For subsequent new or modified posts, we will read the last 50 modified posts in the view controller’s load in a throttled manner using a great library called RateLimit:
class HomeViewController: UIViewController { let refreshLimit = TimedLimiter(limit: 10800) override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) // Retrieve latest posts not more than every X hours refreshLimit.execute { service.updateFromRemote() } } }
Great, now we should everything we need to build the rest of the app!
The UI
Since a few pages will need to display posts, such favorites, most popular, and just browsing all or by category, we should create a couple base classes. One for tables and another for collections. Here’s what the base table class called “RealmPostTableViewController” looks like:
class RealmPostTableViewController: UITableViewController, RealmControllable { var notificationToken: NotificationToken? var models: Results<Post>? let service = PostService() let cellNibName: String? = "PostTableViewCell" var categoryID: Int = 0 { didSet { applyFilterAndSort(categoryID > 0 ? "ANY categories.id == \(categoryID)" : nil) didCategorySelect() } } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) // Retrieve latest posts not more than every X hours AppGlobal.postRefreshLimit.execute { service.updateFromRemote() } } func didCategorySelect() { // Override in derived classes if needed } } extension RealmPostTableViewController { override func numberOfSections(in tableView: UITableView) -> Int { return 1 } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return models?.count ?? 0 } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView[indexPath] as! PostTableViewCell guard let model = models?[indexPath.row] else { return cell } return cell.bind(model) } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { performSegue(withIdentifier: PostDetailViewController.segueIdentifier, sender: nil) } }
To create the counterpart collection base class, I’m using a protocol called “RealmControllable” to unify the table and collection views. I cover this in my previous post called “Protocol-Oriented TableView and CollectionView in Swift“.
protocol RealmControllable: DataControllable { associatedtype ServiceType: Serviceable associatedtype DataType: Object var notificationToken: NotificationToken? { get set } var service: ServiceType { get } var models: Results<DataType>? { get set } } extension RealmControllable { var sortProperty: String { return "date" } var sortAscending: Bool { return false } func setupDataSource() { models = AppGlobal.realm?.objects(DataType.self).sorted( byKeyPath: sortProperty, ascending: sortAscending) // Set results notification block notificationToken = models?.addNotificationBlock { [unowned self] (changes: RealmCollectionChange) in switch changes { case .initial, .update: self.dataView.reloadData() case .error(let err): // An error occurred while opening the Realm file // on the background worker thread fatalError("\(err)") } } dataView.reloadData() } func applyFilterAndSort(_ filter: String? = nil, sort: String? = nil, ascending: Bool? = nil) { guard let realm = AppGlobal.realm else { return } var temp = realm.objects(DataType.self) .sorted(byKeyPath: sortProperty, ascending: ascending ?? sortAscending) if let filter = filter, !filter.isEmpty { temp = temp.filter(filter) } if let sort = sort, !sort.isEmpty { temp = temp.sorted(byKeyPath: sort, ascending: ascending ?? sortAscending) } models = temp dataView.reloadData() dataView.scrollToTop() } }
The Realm “NotificationToken” is wiring up the database updates reactively to the table or collection views. When an update happens in the database, it reloads the UI automatically. Also, there is filter and sort functions that the derived view controllers can use easily.
For the table cell, we are simply binding the data to the cell view and configuring its style:
public class PostTableViewCell: UITableViewCell { @IBOutlet public weak var itemImage: UIImageView! @IBOutlet public weak var itemTitle: UILabel! @IBOutlet public weak var itemContent: UILabel! public func bind(_ model: Post) -> Self { configure() itemTitle.text = model.title.decodeHTML() itemContent.text = model.excerpt.decodeHTML().stripHTML() itemImage.setURL(model.media?.link) return self } func configure() { // Optimize layer.shouldRasterize = true layer.rasterizationScale = UIScreen.main.scale // Style itemImage.layer.shadowOffset = .zero itemImage.layer.shadowRadius = 1 itemImage.layer.shadowOpacity = 1 itemImage.layer.masksToBounds = false } }
Finally for the view controller, we will end up with something super lightweight and flexible:
class PopularViewController: RealmPostTableViewController { override func viewDidLoad() { super.viewDidLoad() // Lock post list to the popular category categoryID = 595 } }
We can use this rapid development pattern for all our view controllers that need to display a list of posts. For example, here’s one that displays a user’s favorite posts:
class FavoritesViewController: RealmPostTableViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) applyFavoriteFilter() } func applyFavoriteFilter(_ reload: Bool = true) { let favorites = AppGlobal.userDefaults[.favorites] .map(String.init) .joined(separator: ",") applyFilterAndSort("id IN {\(favorites)}", reload: reload) } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return AppGlobal.userDefaults[.favorites].count } override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { return true } override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) { if let model = models?[indexPath.row], editingStyle == .delete { service.removeFavorite(model.id) tableView.deleteRows(at: [indexPath], with: .fade) applyFavoriteFilter(false) } } override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { return models?.count ?? 0 == 0 ? "No favorites to show." : nil } }
The App
After getting all our framework pieces in place, our apps will be super lightweight but still configurable. Here’s what an app using the framework will look like:

That’s incredibly light! Updating the framework underneath would update the all apps across all our projects at once. For wiring it to the framework, all that’s needed is the “Settings.plist” to store your app and WordPress configurations, and an HTML file used as a template for your post detail views (using Stencil to bind the values to the HTML). Of course, finally the AppDelegate code to glue the framework to the app’s lifecycle:
class AppDelegate: UIResponder, UIApplicationDelegate, AppPressable { var window: UIWindow? override init() { super.init() AppGlobal.userDefaults.registerSite() } func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { return didFinishLaunchingSite(application, launchOptions: launchOptions) } func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool { return continueUserActivity(application, userActivity: userActivity, restorationHandler: restorationHandler) } }
That’s it! Here’s what the app looks like with only a few files in the app source code:

Conclusion
Although this post was about WordPress, the concepts are generic enough to extend the framework to support different providers. This would allow integrating other CMS or REST endpoint with the same code. As long as the JSON structure is the same then you’re good, but even the JSON structure can be abstracted away under each provider model for even more flexibility.
To view the evolving working source code, see SwiftyPress GitHub repo.
Happy Coding!!
thank you for this work but i tried somethings but there is a problem with realm like this “module compiled with Swift 3.0.2 can not be imported in swift 3.1” i download new version of realm and changed embedded binaries but there is no changing
I’ve recently updated the project and dependencies to Swift 3.1, including Realm. In your app project, you should be grabbing SwiftyPress as a dependency via Carthage for easy updates. Then you can update SwiftyPress for the app along with all its associated dependencies. Sometimes the nested dependencies don’t update quick enough to the newest Swift version. So you can make Carthage just fetch the dependencies first then build locally. First run `carthage update –platform iOS –no-build` then `carthage build –platform iOS`.
I’ve updated the read me at the SwiftyPress repo to help a bit more. Also, you can take a look at a sample app wired to use SwiftyPress as a Carthage dependency: https://github.com/basememara/BasemEmaraBlogIOS. I hope this helps and let me know if you still have any questions from here.
Hello Basem,
Many thanks for sharing your experience and your work !
I got the source of your apps here https://github.com/basememara/BasemEmaraBlogIOS, unfortunately when I build with these commands carthage update –platform iOS –no-build then carthage build –platform iOS I got this message :
Task failed with exit code 65:
/usr/bin/xcrun xcodebuild -workspace /Users/jerome/Downloads/BasemEmaraBlogIOS-master/Carthage/Checkouts/SwiftyPress/SwiftyPress.xcworkspace -scheme “SwiftyPress iOS” -configuration Release -derivedDataPath /Users/jerome/Library/Caches/org.carthage.CarthageKit/DerivedData/SwiftyPress/77718404f1ce7b27bf370a5537dc784d283c5195 -sdk iphoneos ONLY_ACTIVE_ARCH=NO BITCODE_GENERATION_MODE=bitcode CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY= CARTHAGE=YES clean build
This usually indicates that project itself failed to compile. Please check the xcodebuild log for more details: /var/folders/4t/zts3_jy95xx9zhws33gkf4cr0000gn/T/carthage-xcodebuild.A58x5X.log
Could you help me please?
Best Regards,
J茅r么me.
Hi J茅r么me, I’m glad you found this useful and thanks for giving this a try.
I looked at the logs for the error and it said: “error: no such module ‘Stencil’ import Stencil”. The issue is Stencil doesn’t provide Carthage support: https://github.com/kylef/Stencil/pull/88
So at the SwiftyPress repo, I outlined the steps to take. Hopefully I can build an easy script around this:
1) `carthage update –platform iOS –no-build`
2) `(cd Carthage/Checkouts/Stencil && swift package generate-xcodeproj)`
3) Open Checkouts/Stencil/Stencil.proj and add iOS 10 in the project “Build Setting” under “iOS Deployment Target”
4) `carthage build –platform iOS`
After you do this the first time, you can skip steps 2 and 3 going forward and just run the following to update the dependencies when needed:
`carthage update –platform iOS –no-build && carthage build –platform iOS`
Let me know if you have any issues from here and I’ll be glad to help.
Hi Basem,
Many Thanks ! That works !
Have a nice day !
J茅r么me.
Wow, this is so cool. I think you can take this a step further and make your collection view controller support multiple cell types (if you want to add ads for instance, between different blog posts).
Also, WordPress comes with the REST plugin integrated already, so there’s no extra effort on the website front. Additionally, you could use the Advanced Custom Fields plugin to add extra info in the JSON response (and potentially being able to avoid the need for SwiftPress plugin).
Thanks for sharing this!
Great suggestions! I totally dig an extensible CollectionView, which would make the apps pretty diverse.
It would be great if I can phase out the SwiftyPress WP plugin, but I haven’t found anything from WP, and the custom ones out there had a few issues. This one comes close to what I was hoping for, but still had issues with null custom field values in the endpoint: https://wordpress.org/plugins/wp-rest-api-controller/
Btw nice site at iOS App Templates. I would love to push the envelope in that space with you.
Great stuff.
I am quiet new to iOS developement at all and trying to use this Framework as a starting point to develop the app for our blog.
I got a few question regarding SwiftyPress so far (just viewed the example project app on a simulator today for a few seconds)
What麓s best practise here to customize the look and feel of the “app”? The views are generated in pure code, no storyboards, graphic assets or similar do i get this right?
How flexible this is to lets say change the look and feel based on a Sketch UI Design template or adding a Material Framework? Adding some custom transitions/animations and so on? For e.g i would like to have a preloader / spinning indicator added to all dynamicly loaded assets like featured images in the table and collection views.
Now my blog also makes use of a custom post type called “releases” to show products created like posts but with some custom fields and taxonomies i would like to view/filter/sort/search/paginate through
Would it be hard to add support for a custom post type like this?
Not exactly sure where and how i would add functionality for this. I guess i would have to adjust the JSON controlling part and the wordpress plugin provided by you.
Last but not least there is no in-app comment or social sharing functionality or? You just link to the external wordpress blog page in the browser?
Hope my questions are not to over killing. And sorry for my bad english 馃槈
best regards and keep up the good work.
Hello Basem,
I got this errors when I started retrieving associated models
categories = List(json[.categories].map(Term.init))
tags = List(json[.tags].map(Term.init))
Argument passed to call that takes no arguments
and these related to Realm in Post and Term service files
try realm.write { realm.add(List(results), update: true) }
Argument passed to call that takes no arguments
Thanks in advance
Hello! Amazing tutorial on using wordpress REST API.
Would you mind making a video highlighting the steps necessary to establish the connection!
Hi, Just using your base code to setup an app for my blog, but getting stuck when running some tests.
I’m getting the following error ‘Use of undeclared type ‘SeedWorkerType’ – could you shed some light on this please? I’m not sure if this is causing the other issue of the seed.json not being updated from my WordPress site.
Cheers
Hello. I’m trying to install https://github.com/ZamzamInc/SwiftyPress, but get error:
Showing All Messages
: the package swiftypress[https://github.com/ZamzamInc/SwiftyPress.git] @ 2.0.2 contains incompatible dependencies:
stencil[git@github.com:ZamzamInc/Stencil.git] @ lite
How I can resolve it?
I dowloaded and run your framework, but get error:
Type ‘TestsModule’ does not conform to protocol ‘SwiftyPressModule’