Connect your iOS app to WordPress with SwiftPresso


Greetings, traveler!

I have always cherished and valued the WordPress community, where individuals assist each other in creative development, including creativity in writing code, because, as we know, code is poetry.

More than 478 million websites are built on WordPress, and WordPress powers almost 44% of all websites on the Internet. Of those websites running on the known CMS, nearly 63% of them are powered by WordPress.

SwiftPresso

Today, I want to introduce a tool I made for WordPress enthusiasts. It’s called SwiftPresso. SwiftPresso allows you to convert a WordPress website to an iOS app with just one line of code.

@main
struct DemoApp: App {
        
    var body: some Scene {
        WindowGroup {
            SwiftPresso.View.postList(host: "livsycode.com")
        }
    }
}

If you prefer full control, you can create your own entities and use SwiftPresso only to manage networking, domain models, and HTML text parsing.

Here is the SwiftPresso GitHub repository.

Built-in views

Let’s consider an example with built-in views. With SwiftPresso, you can display your post list and post details. There are two ways to display post details in SwiftPresso:

  • Using HTML content mapped to an NSAttributedString.
  • Using WKWebView.

To choose between these options, you can set the configuration value.

SwiftPresso.View.postList(
    host: "livsycode.com",
    isShowContentInWebView: true
)

Important

If you prefer the first option, remember to edit your Info.plist file to add a permission string about photo library usage since SwiftPresso allows users to save images to a camera roll. Select “Privacy – Photo Library Additions Usage Description” to add this string.

You can also enable or disable displaying a post featured image. To do this, you can configure the isShowFeaturedImage parameter.

SwiftPresso.View.postList(
    host: "livsycode.com",
    isShowFeaturedImage: true
)

You can also configure text color, background color, font, etc. Here is the complete list of the parameters.

host // API host.
httpScheme // API HTTP scheme.
postsPerPage // Posts per page in the post list request.
tagPathComponent // API Tag path component.
categoryPathComponent // API Category path component.
isShowContentInWebView // Using WKWebView as post view.
isShowFeaturedImage // Post featured image visibility.
backgroundColor // Post list and post view background color.
interfaceColor // Post list and post view interface color.
textColor // Post list and post view text color.
postListFont // Post list font.
postBodyFont // Post body font.
postTitleFont // Post title font.
menuBackgroundColor // Menu background color.
menuTextColor // Menu text color.
homeIcon // The icon for the navigation bar button that restores the interface to its default state.
homeTitle // Navigation title for default state.
searchTitle // Navigation title for search state.
isShowPageMenu // Determines the visibility of the page menu.
isShowTagMenu // Determines the visibility of the tag menu.
isShowCategoryMenu // Determines the visibility of the category menu.
pageMenuTitle // Determines the title of the page menu.
tagMenuTitle // Determines the title of the tag menu.
categoryMenuTitle // Determines the title of the category menu.
isParseHTMLWithYouTubePreviews // If an HTML text contains a link to a YouTube video, it will be displayed as a preview of that video with an active link.
isExcludeWebHeaderAndFooter // Remove web page's header and footer.
isMenuExpanded // To expand menu items by default.

To change the default loading indicator, you can use the placeholder parameter:

SwiftPresso.View.postList(host: "livsycode.com") {
    ProgressView()
}

If you prefer using WebView, you can exclude the header and footer from your web pages and configure the appearance of UI elements.

SwiftPresso.View.postList(
    host: "livsycode.com",
    isExcludeWebHeaderAndFooter: true
)

WebView will open internal website links, while external links will be handled via the device’s default web browser.

You can also manage links to categories and tags. If you want such a link to lead to the application post list view, you must change the permalinks in the WordPress admin panel. You can also handle such links if you want to show post details without using WebView. If there are links to other posts within your posts, you can open them without leaving the app. You can enable this feature by changing the permalink structure in the WordPress admin settings as well.

Permalinks

To do this, go to the WordPress admin panel and open Settings -> Permalinks.

You can specify your permalinks like this to allow SwiftPresso to handle tag and category links.

/%category%/%postname%/

You can change the path components of your choice. Just remember to set preferences via the SwiftPresso configure method.

SwiftPresso.View.postList(
    host: "livsycode.com",
    tagPathComponent: "series",
    categoryPathComponent: "topics"
)

If you use the built-in PostView and want to handle post links inside the application, you must provide information about the post ID at the end of the URL. Thus, you can prescribe, for example, such a scheme:

/%category%/%postname%/%post_id%/

Important

Changing permalinks will affect your SEO settings, so do this cautiously.

Configuration

If you use SwiftPresso built-in views, you don’t need to worry about the configuration method calling. However, when utilizing SwiftPresso entities individually, you must manually configure the data. To do this, you can use the configure method.

SwiftPresso.configure(
    host: "livsycode.com",
    httpScheme: .https
)
    static func configure(
        host: String,
        httpScheme: HTTPScheme = .https,
        postsPerPage: Int = 50,
        tagPathComponent: String = "tag",
        categoryPathComponent: String = "category",
        isShowContentInWebView: Bool = false,
        isShowFeaturedImage: Bool = true,
        postListFont: Font = .title2,
        postBodyFont: UIFont = .systemFont(ofSize: 17),
        postTitleFont: Font = .largeTitle,
        backgroundColor: Color = Color(uiColor: .systemBackground),
        interfaceColor: Color = .primary,
        textColor: Color = .primary,
        menuBackgroundColor: Color = .primary,
        menuTextColor: Color = Color(uiColor: .systemBackground),
        homeIcon: Image = Image(systemName: "house"),
        homeTitle: String = "Home",
        searchTitle: String = "Search",
        isShowPageMenu: Bool = true,
        isShowTagMenu: Bool = true,
        isShowCategoryMenu: Bool = true,
        pageMenuTitle: String = "Pages",
        tagMenuTitle: String = "Tags",
        categoryMenuTitle: String = "Category",
        isParseHTMLWithYouTubePreviews: Bool = true,
        isExcludeWebHeaderAndFooter: Bool = true,
        isMenuExpanded: Bool = true
    ) { ... }

The SwiftPresso.Configuration stores values to manage API requests and handle UI appearance.

let host = SwiftPresso.Configuration.API.host
let color = SwiftPresso.Configuration.UI.backgroundColor
public enum Configuration {
    
    public enum API {
        
        /// API host.
        public static var host: String { get }
        
        /// Posts per page in the post list request.
        public static var postsPerPage: Int { get }
        
        /// API HTTP scheme.
        public static var httpScheme: HTTPScheme { get }
        
        /// API Tag path component.
        public static var tagPathComponent: String { get }
        
        /// API Category path component.
        public static var categoryPathComponent: String { get }
        
    }
    
    public enum UI {
        
        /// Remove web page's header and footer.
        public static var isExcludeWebHeaderAndFooter: Bool { get }
        
        /// Post list and post view background color.
        public static var backgroundColor: Color { get }
        
        /// Post list font.
        public static var postListFont: Font { get }
        
        /// Post body font.
        public static var postBodyFont: UIFont { get }
        
        /// Post title font.
        public static var postTitleFont: Font { get }
        
        /// Post list and post view interface color.
        public static var interfaceColor: Color { get }
        
        /// Post list and post view text color.
        public static var textColor: Color { get }
        
        /// Determines the visibility of the page menu.
        public static var isShowPageMenu: Bool { get }
        
        /// Post featured image visibility.
        public static var isShowFeaturedImage: Bool { get }
        
        /// Determines the visibility of the tag menu.
        public static var isShowTagMenu: Bool { get }
        
        /// Determines the visibility of the category menu.
        public static var isShowCategoryMenu: Bool { get }
        
        /// The icon for the navigation bar button that restores the interface to its default state.
        public static var homeIcon: Image { get }
        
        /// Determines the title of the page menu.
        public static var pageMenuTitle: String { get }
        
        /// Determines the title of the tag menu.
        public static var tagMenuTitle: String { get }
        
        /// Determines the title of the category menu.
        public static var categoryMenuTitle: String { get }
        
        /// Navigation title for default state.
        public static var homeTitle: String { get }
        
        /// Navigation title for search state.
        public static var searchTitle: String { get }
        
        /// Menu background color.
        public static var menuBackgroundColor: Color { get }
        
        /// Menu text color.
        public static var menuTextColor: Color { get }
        
        /// To expand menu items by default.
        public static var isMenuExpanded: Bool { get }
        
        /// Using WKWebView as post view.
        public static var isShowContentInWebView: Bool { get }
        
        /// If an HTML text contains a link to a YouTube video, it will be displayed as a preview of that video with an active link.
        public static var isParseHTMLWithYouTubePreviews: Bool { get }
        
    }
    
}

Data Providers

To use SwiftPresso providers, you can inject them via SwiftPresso property wrappers.

class ViewModel {
    
    @SwiftPressoInjected(\.categoryListProvider)
    var categoryListProvider: CategoryListProviderProtocol
    
}

Alternatively, you can use the SwiftPresso factory methods.

let postListProvider = SwiftPresso.Provider.postListProvider()

To use property wrappers inside an Observable class, you can mark properties with the @ObservationIgnored Macro.

@Observable
class ViewModel {
    
    @ObservationIgnored
    @SwiftPressoInjected(\.categoryListProvider)
    var categoryListProvider: CategoryListProviderProtocol
    
}

SwiftPresso offers several data providers. For now, you can communicate with these endpoints:

enum Endpoint {
    case posts
    case categories
    case tags
    case pages
}

Thus, there are six providers in SwiftPresso:

  • Post list provider.
  • Single post provider.
  • Page list provider.
  • Single page provider.
  • Category list provider.
  • Tag list provider.

Post List Provider

The post list provider has two methods for retrieving an array of post models, which return two different types of post models in SwiftPresso.

  • The first one provides a lightweight model, which includes only crucial data.
  • The second provider returns an array of raw data models.

You can configure several parameters to perform the request:

  • Page number.
  • Posts per page.
  • Search terms.
  • Categories
  • Tags.
  • Post IDs.
class ViewModel {
    
    @SwiftPressoInjected(\.postListProvider)
    var postListProvider: PostListProviderProtocol
    
    func getData() async {
        let data = try? await postListProvider.getPosts(
            pageNumber: 1,
            perPage: 20,
            searchTerms: "some text...",
            categories: [1,2,3],
            tags: [1,2,3],
            includeIDs: [1,22]
        )
        
        let rawData = try? await postListProvider.getRawPosts(
            pageNumber: 1,
            perPage: 20,
            searchTerms: "some text...",
            categories: [1,2,3],
            tags: [1,2,3],
            includeIDs: [1,22]
        )
    }
    
}

Single post Provider

The single post provider has two methods for retrieving a specific post model.

  • The first one provides a lightweight model, which includes only crucial data.
  • The second provider returns the raw data model.
class ViewModel {
    
    @SwiftPressoInjected(\.postProvider)
    var postProvider: PostProviderProtocol
        
    func getData() async {
        let data = try? await postProvider.getPost(id: 55)
        let rawData = try? await postProvider.getRawPost(id: 55)
    }
    
}

Page List Provider

As the post list provider, the page list provider also has two methods for retrieving an array of page models, which return two types of page models in SwiftPresso.

  • The first one provides a lightweight model, which includes only crucial data.
  • The second provider returns an array of raw data models.
class ViewModel {
    
    @SwiftPressoInjected(\.pageListProvider)
    var pageListProvider: PageListProviderProtocol
        
    func getData() async {
        let data = try? await pageListProvider.getPages()
        let rawData = try? await pageListProvider.getRawPages()
    }
    
}

Single Page Provider

The page provider also has two methods for retrieving a page model.

  • The first one provides a lightweight model, which includes only crucial data.
  • The second provider returns the raw data model.
class ViewModel {
    
    @SwiftPressoInjected(\.pageProvider)
    var pageProvider: PageProviderProtocol
        
    func getData() async {
        let data = try? await pageProvider.getPage(id: 22)
        let rawData = try? await pageProvider.getRawPage(id: 22)
    }
    
}

Category / Tag List Provider

The category and the tag list providers provide an array of models, which is a Category type.

class ViewModel {
    
    @SwiftPressoInjected(\.categoryListProvider)
    var categoryListProvider: CategoryListProviderProtocol
    
    func getData() async {
        let data = try? await categoryListProvider.getCategories()
    }
    
}
class ViewModel {
    
    @SwiftPressoInjected(\.tagListProvider)
    var tagListProvider: TagListProviderProtocol
        
    func getData() async {
        let data = try? await tagListProvider.getTags()
    }
    
}

HTML mapper

The HTML mapper can transform an HTML text into an NSAttributedString. If an HTML text contains a link to a YouTube video, it will be displayed as a preview of that video with a clickable link.

let mapper = SwiftPresso.Mapper.htmlMapper()

func map(post: PostModel) -> NSAttributedString {
    mapper.attributedStringFrom(
        htmlText: post.content,
        color: .black,
        font: .systemFont(ofSize: 17),
        width: 375,
        isHandleYouTubeVideos: true
    )
}
@SwiftPressoInjected(\.htmlMapper)
var mapper: HTMLMapperProtocol

func map(post: PostModel) -> NSAttributedString {
    mapper.attributedStringFrom(
        htmlText: post.content,
        color: .black,
        font: .systemFont(ofSize: 17),
        width: 375,
        isHandleYouTubeVideos: true
    )
}

Conclusion

I hope this tool will be helpful. It was made with love and respect for the WordPress community.