Methods to make a PWA for iOS?
A progressive internet software is only a particular type of web site, that may look and behave like a local iOS app. With a view to construct a PWA, first we will create an everyday web site utilizing SwiftHtml. We will begin with an everyday executable Swift bundle with the next dependencies.
import PackageDescription
let bundle = Package deal(
title: "Instance",
platforms: [
.macOS(.v12)
],
dependencies: [
.package(url: "https://github.com/binarybirds/swift-html", from: "1.2.0"),
.package(url: "https://github.com/vapor/vapor", from: "4.54.0"),
],
targets: [
.executableTarget(name: "Example", dependencies: [
.product(name: "SwiftHtml", package: "swift-html"),
.product(name: "Vapor", package: "vapor"),
]),
.testTarget(title: "ExampleTests", dependencies: ["Example"]),
]
)
As you possibly can see we will use the vapor Vapor library to serve our HTML web site. If you do not know a lot about Vapor let’s simply say that it’s a internet software framework, which can be utilized to construct server aspect Swift functions, it is a fairly superb instrument I’ve a newbie’s information publish about it.
After all we will want some parts for rendering views utilizing SwiftHtml, you need to use the supply snippets from my earlier article, however right here it’s once more how the SwiftHtml-based template engine ought to seem like. It’s best to learn my different article if you wish to know extra about it. 🤓
import Vapor
import SwiftSgml
public protocol TemplateRepresentable {
@TagBuilder
func render(_ req: Request) -> Tag
}
public struct TemplateRenderer {
var req: Request
init(_ req: Request) {
self.req = req
}
public func renderHtml(_ template: TemplateRepresentable, minify: Bool = false, indent: Int = 4) -> Response {
let doc = Doc(.html) { template.render(req) }
let physique = DocumentRenderer(minify: minify, indent: indent).render(doc)
return Response(standing: .okay, headers: ["content-type": "text/html"], physique: .init(string: physique))
}
}
public extension Request {
var templates: TemplateRenderer { .init(self) }
}
We’re additionally going to wish an index template for our essential HTML doc. Since we’re utilizing a Swift DSL to put in writing HTML code we do not have to fret an excessive amount of about mistyping a tag, the compiler will defend us and helps to keep up a very legitimate HTML construction.
import Vapor
import SwiftHtml
struct IndexContext {
let title: String
let message: String
}
struct IndexTemplate: TemplateRepresentable {
let context: IndexContext
init(_ context: IndexContext) {
self.context = context
}
func render(_ req: Request) -> Tag {
Html {
Head {
Title(context.title)
Meta().charset("utf-8")
Meta().title(.viewport).content material("width=device-width, initial-scale=1")
}
Physique {
Fundamental {
Div {
H1(context.title)
P(context.message)
}
}
}
}
}
}
Lastly we are able to merely render the bootstrap our Vapor server occasion, register our route handler and render the index template inside the primary entry level of our Swift bundle by utilizing the beforehand outlined template helper strategies on the Request object.
import Vapor
import SwiftHtml
var env = strive Surroundings.detect()
strive LoggingSystem.bootstrap(from: &env)
let app = Utility(env)
defer { app.shutdown() }
app.get { req -> Response in
let template = IndexTemplate(.init(title: "Howdy, World!",
message: "This web page was generated by the SwiftHtml library."))
return req.templates.renderHtml(template)
}
strive app.run()
It’s simply that simple to setup and bootstrap a completely working internet server that’s able to rendering a HTML doc utilizing the ability of Swift and the Vapor framework. In the event you run the app it is best to have the ability to see a working web site by visiting the http://localhost:8080/ tackle.
Turning an internet site into an actual iOS PWA
Now if we need to rework our web site right into a standalone PWA, now we have to offer a hyperlink a particular internet app manifest file inside the pinnacle part of the index template.
Meta tags vs manifest.json
Looks as if Apple follows type of a wierd route if it involves PWA help. They’ve fairly a historical past of “pondering exterior of the field”, this mindset applies to progressive internet apps on iOS, since they do not are likely to observe the requirements in any respect. For Android units you could possibly create a manifest.json file with some predefined keys and you would be simply superb together with your PWA. Then again Apple these days prefers varied HTML meta tags as a substitute of the net manifest format.
Personally I do not like this strategy, as a result of your HTML code might be bloated with all of the PWA associated stuff (as you may see that is going to occur if it involves launch display screen photos) and I consider it is higher to separate these type of issues, however hey it is Apple, they cannot be unsuitable, proper? 😅
In any case, let me present you the best way to help varied PWA options on iOS.
Enabling standalone app mode
The very first few keys that we might like so as to add to the index template has the apple-mobile-web-app-capable title and it is best to use the “sure” string as content material. This may point out that the app ought to run in full-screen mode, in any other case it may be displayed utilizing Safari identical to an everyday web site.
struct IndexTemplate: TemplateRepresentable {
let context: IndexContext
init(_ context: IndexContext) {
self.context = context
}
func render(_ req: Request) -> Tag {
Html {
Head {
Title(context.title)
Meta().charset("utf-8")
Meta().title(.viewport).content material("width=device-width, initial-scale=1")
Meta()
.title(.appleMobileWebAppCapable)
.content material("sure")
}
Physique {
Fundamental {
Div {
H1(context.title)
P(context.message)
}
}
}
}
}
}
We should always change the hostname of the server and pay attention on the 0.0.0.0 tackle, this manner in case your cellphone is on the identical native WiFi community it is best to have the ability to attain your internet server straight.
import Vapor
import SwiftHtml
var env = strive Surroundings.detect()
strive LoggingSystem.bootstrap(from: &env)
let app = Utility(env)
defer { app.shutdown() }
app.http.server.configuration.hostname = "0.0.0.0"
if let hostname = Surroundings.get("SERVER_HOSTNAME") {
app.http.server.configuration.hostname = hostname
}
app.get { req -> Response in
let template = IndexTemplate(.init(title: "Howdy, World!",
message: "This web page was generated by the SwiftHtml library."))
return req.templates.renderHtml(template)
}
strive app.run()
You’ll find out your native IP tackle by typing the next command into the Terminal app.
# utilizing ifconfig & grep
ifconfig | grep -Eo 'inet (addr:)?([0-9]*.){3}[0-9]*' | grep -Eo '([0-9]*.){3}[0-9]*' | grep -v '127.0.0.1'
# utilizing ifconfig & sed
ifconfig | sed -En 's/127.0.0.1//;s/.*inet (addr:)?(([0-9]*.){3}[0-9]*).*/2/p'
Simply use that IP tackle and go to the http://[ip-address]:8080/ web site utilizing your iOS machine, then it is best to have the ability to add your web site to your own home display screen as a bookmark. Simply faucet the Share icon utilizing Safari and choose the Add to House Display screen menu merchandise from the listing. On the brand new display screen faucet the Add button on the highest proper nook, this can create a brand new icon on your own home display screen as a bookmark to your web page. Optionally, you possibly can present a customized title for the bookmark. ☺️
Since we have added the meta tag, should you contact the newly created icon it ought to open the webpage as a standalone app (with out utilizing the browser). After all the app continues to be only a web site rendered utilizing an online view. The standing bar will not match the white background and it has no customized icon or splash display screen but, however we will repair these points proper now. 📱
Customized title and icon
To offer a customized title we simply have so as to add a brand new meta tag, fortuitously the SwiftHtml library has predefined enums for all of the Apple associated meta names, so you do not have to sort that a lot. The icon state of affairs is a little more troublesome, since now we have so as to add a bunch of measurement variants.
struct IndexTemplate: TemplateRepresentable {
let context: IndexContext
init(_ context: IndexContext) {
self.context = context
}
func render(_ req: Request) -> Tag {
Html {
Head {
Title(context.title)
Meta().charset("utf-8")
Meta().title(.viewport).content material("width=device-width, initial-scale=1")
Meta()
.title(.appleMobileWebAppCapable)
.content material("sure")
Meta()
.title(.appleMobileWebAppTitle)
.content material("Howdy PWA")
Hyperlink(rel: .appleTouchIcon)
.href("/img/apple/icons/192.png")
for measurement in [57, 72, 76, 114, 120, 144, 152, 180] {
Hyperlink(rel: .appleTouchIcon)
.sizes("(measurement)x(measurement)")
.href("/img/apple/icons/(measurement).png")
}
}
Physique {
Fundamental {
Div {
H1(context.title)
P(context.message)
}
}
}
}
}
}
As you possibly can see icons are referenced by utilizing the Hyperlink tag, utilizing the Apple contact icon rel attribute. The default icon with out the sizes attribute could be a 192×192 pixel picture, plus I am offering some smaller sizes by utilizing a for loop right here. We additionally must serve these icon recordsdata by utilizing Vapor, that is why we will alter the configuration file and allow the FileFiddleware.
import Vapor
import SwiftHtml
var env = strive Surroundings.detect()
strive LoggingSystem.bootstrap(from: &env)
let app = Utility(env)
defer { app.shutdown() }
app.middleware.use(FileMiddleware(publicDirectory: app.listing.publicDirectory))
app.http.server.configuration.hostname = "0.0.0.0"
if let hostname = Surroundings.get("SERVER_HOSTNAME") {
app.http.server.configuration.hostname = hostname
}
app.get { req -> Response in
let template = IndexTemplate(.init(title: "Howdy, World!",
message: "This web page was generated by the SwiftHtml library."))
return req.templates.renderHtml(template)
}
strive app.run()
By including the FileMiddleware to the app with the general public listing path configuration your server app is ready to serve static recordsdata from the Public listing. Be at liberty to create it and place the app icons below the Public/img/apple/icons folder. In case you are working the server from the command line you may be superb, however if you’re utilizing Xcode it’s important to specify a customized working listing for Vapor, this can enable the system to lookup the general public recordsdata from the appropriate place.
Your customized icons will not present up if you’re utilizing a self-signed certificates.
Construct and run the server and attempt to bookmark your web page once more utilizing your cellphone. Once you see the add bookmark web page it is best to have the ability to validate that the app now makes use of the predefined Howdy PWA title and the picture preview ought to present the customized icon file as a substitute of a screenshot of the web page.
Correct standing bar shade for iOS PWAs
Lengthy story brief, there’s a nice article on CSS-Methods about the newest model of Safari and the way it handles varied theme colours on totally different platforms. It is a fantastic article, it is best to positively learn it, however in a lot of the instances you will not want this a lot data, however you merely need to help gentle and darkish mode to your progressive internet app. That is what I’ll present you right here.
For gentle mode we will use a white background shade and for darkish mode we use black. We’re additionally going to hyperlink a brand new fashion.css file so we are able to change the background of the positioning and the font shade in accordance with the present shade scheme. First, the brand new meta tags to help theme colours each for gentle and darkish mode.
struct IndexTemplate: TemplateRepresentable {
let context: IndexContext
init(_ context: IndexContext) {
self.context = context
}
func render(_ req: Request) -> Tag {
Html {
Head {
Title(context.title)
Meta().charset("utf-8")
Meta().title(.viewport).content material("width=device-width, initial-scale=1")
Meta()
.title(.appleMobileWebAppCapable)
.content material("sure")
Meta()
.title(.appleMobileWebAppTitle)
.content material("Howdy PWA")
Meta()
.title(.colorScheme)
.content material("gentle darkish")
Meta()
.title(.themeColor)
.content material("#fff")
.media(.prefersColorScheme(.gentle))
Meta()
.title(.themeColor)
.content material("#000")
.media(.prefersColorScheme(.darkish))
Hyperlink(rel: .stylesheet)
.href("/css/fashion.css")
Hyperlink(rel: .appleTouchIcon)
.href("/img/apple/icons/192.png")
for measurement in [57, 72, 76, 114, 120, 144, 152, 180] {
Hyperlink(rel: .appleTouchIcon)
.sizes("(measurement)x(measurement)")
.href("/img/apple/icons/(measurement).png")
}
}
Physique {
Fundamental {
Div {
H1(context.title)
P(context.message)
}
}
}
}
}
}
Contained in the fashion CSS file we are able to use a media question to detect the popular shade scheme, identical to we did it for the .themeColor meta tag utilizing SwiftHtml.
physique {
background: #fff;
shade: #000;
}
@media (prefers-color-scheme: darkish) {
physique {
background: #000;
shade: #fff;
}
}
That is it, now the standing bar ought to use the identical shade as your essential background. Attempt to change between darkish and light-weight mode and ensure every little thing works, there’s a cool PWA demo challenge right here with totally different colours for every mode if you wish to double test the code. ✅
Splash display screen help
Trace: it is ridiculous. Splash screens on iOS are problematic. Even native apps are likely to cache the unsuitable splash display screen or will not render PNG recordsdata correctly, now if it involves PWAs this is not essential higher. I used to be in a position to present splash display screen photos for my app, however it took me fairly some time and switching between darkish and light-weight mode is completely damaged (so far as I do know it). 😅
With a view to cowl each single machine display screen measurement, it’s important to add a lot of linked splash photos to your markup. It is so ugly I even needed to create a bunch of extension strategies to my index template.
extension IndexTemplate {
@TagBuilder
func splashTags() -> [Tag] {
splash(320, 568, 2, .panorama)
splash(320, 568, 2, .portrait)
splash(414, 896, 3, .panorama)
splash(414, 896, 2, .panorama)
splash(375, 812, 3, .portrait)
splash(414, 896, 2, .portrait)
splash(375, 812, 3, .panorama)
splash(414, 736, 3, .portrait)
splash(414, 736, 3, .panorama)
splash(375, 667, 2, .panorama)
splash(375, 667, 2, .portrait)
splash(1024, 1366, 2, .panorama)
splash(1024, 1366, 2, .portrait)
splash(834, 1194, 2, .panorama)
splash(834, 1194, 2, .portrait)
splash(834, 1112, 2, .panorama)
splash(414, 896, 3, .portrait)
splash(834, 1112, 2, .portrait)
splash(768, 1024, 2, .portrait)
splash(768, 1024, 2, .panorama)
}
@TagBuilder
func splash(_ width: Int,
_ top: Int,
_ ratio: Int,
_ orientation: MediaQuery.Orientation) -> Tag {
splashTag(.gentle, width, top, ratio, orientation)
splashTag(.darkish, width, top, ratio, orientation)
}
func splashTag(_ mode: MediaQuery.ColorScheme,
_ width: Int,
_ top: Int,
_ ratio: Int,
_ orientation: MediaQuery.Orientation) -> Tag {
Hyperlink(rel: .appleTouchStartupImage)
.media([
.prefersColorScheme(mode),
.deviceWidth(px: width),
.deviceHeight(px: height),
.webkitDevicePixelRatio(ratio),
.orientation(orientation),
])
.href("/img/apple/splash/(calc(width, top, ratio, orientation))(mode == .gentle ? "" : "_dark").png")
}
func calc(_ width: Int,
_ top: Int,
_ ratio: Int,
_ orientation: MediaQuery.Orientation) -> String {
let w = String(width * ratio)
let h = String(top * ratio)
change orientation {
case .portrait:
return w + "x" + h
case .panorama:
return h + "x" + w
}
}
}
Now I can merely add the splashTags() name into the pinnacle part, however I am undecided if the result’s one thing I can completely agree with. Right here, check out the top of this tutorial about splash screens, the code required to help iOS splash screens could be very lengthy and I have not even advised you concerning the 40 totally different picture recordsdata that you’re going to want. Persons are actually utilizing PWA asset turbines to scale back the time wanted to generate these type of photos, as a result of it is fairly uncontrolled. 💩
Secure space & the notch
A particular subject I might like to speak about is the secure space help and the notch. I can extremely advocate to learn this text on CSS-Methods about The Notch and CSS first, however the primary trick is that we are able to use 4 environmental variables in CSS to set correct margin and padding values.
First now we have to alter the viewport meta tag and lengthen our web page past the secure space. This may be executed by utilizing the viewport-fit cowl worth. Contained in the physique of the template we will add a header and a footer part, these areas can have customized background colours and fill the display screen.
struct IndexTemplate: TemplateRepresentable {
let context: IndexContext
init(_ context: IndexContext) {
self.context = context
}
func render(_ req: Request) -> Tag {
Html {
Head {
Title(context.title)
Meta()
.charset("utf-8")
Meta()
.title(.viewport)
.content material("width=device-width, initial-scale=1, viewport-fit=cowl")
Meta()
.title(.appleMobileWebAppCapable)
.content material("sure")
Meta()
.title(.appleMobileWebAppTitle)
.content material("Howdy PWA")
Meta()
.title(.colorScheme)
.content material("gentle darkish")
Meta()
.title(.themeColor)
.content material("#fff")
.media(.prefersColorScheme(.gentle))
Meta()
.title(.themeColor)
.content material("#000")
.media(.prefersColorScheme(.darkish))
Hyperlink(rel: .stylesheet)
.href("/css/fashion.css")
Hyperlink(rel: .appleTouchIcon)
.href("/img/apple/icons/192.png")
for measurement in [57, 72, 76, 114, 120, 144, 152, 180] {
Hyperlink(rel: .appleTouchIcon)
.sizes("(measurement)x(measurement)")
.href("/img/apple/icons/(measurement).png")
}
splashTags()
}
Physique {
Header {
Div {
P("Header space")
}
.class("safe-area")
}
Fundamental {
Div {
Div {
H1(context.title)
for _ in 0...42 {
P(context.message)
}
A("Refresh web page")
.href("/")
}
.class("wrapper")
}
.class("safe-area")
}
Footer {
Div {
P("Footer space")
}
.class("safe-area")
}
}
}
}
}
Besides the background shade we do not need different content material to circulation exterior the secure space, so we are able to outline a brand new CSS class and place some margins on it based mostly on the surroundings. Additionally we can safely use the calc CSS perform if we need to add some further worth to the surroundings.
* {
margin: 0;
padding: 0;
}
physique {
background: #fff;
shade: #000;
}
header, footer {
padding: 1rem;
}
header {
background: #eee;
}
footer {
background: #eee;
padding-bottom: calc(1rem + env(safe-area-inset-bottom));
}
.safe-area {
margin: 0 env(safe-area-inset-right) 0 env(safe-area-inset-left);
}
.wrapper {
padding: 1rem;
}
@media (prefers-color-scheme: darkish) {
physique {
background: #000;
shade: #fff;
}
header {
background: #222;
}
footer {
background: #222;
}
}
It appears to be like good, however what if we might like to make use of customized types for the PWA model solely?
Detecting standalone mode
If you wish to use the show mode media question in your CSS file now we have so as to add a manifest file to our PWA. Yep, that is proper, I’ve talked about earlier than that Apple prefers to make use of meta tags and hyperlinks, however if you wish to use a CSS media question to test if the app runs in a standalone mode you may need to create an online manifest.json file with the next contents.
{
"show": "standalone"
}
Subsequent it’s important to present a hyperlink to the manifest file contained in the template file.
struct IndexTemplate: TemplateRepresentable {
func render(_ req: Request) -> Tag {
Html {
Head {
splashTags()
Hyperlink(rel: .manifest)
.href("/manifest.json")
}
Physique {
}
}
}
}
Within the CSS file now you need to use the display-mode selector to test if the app is working in a standalone mode, you possibly can even mix these selectors and detect standalone mode and darkish mode utilizing a single question. Media queries are fairly helpful. 😍
/* ... */
@media (display-mode: standalone) {
header, footer {
background: #fff;
}
header {
place: sticky;
prime: 0;
border-bottom: 1px strong #eee;
}
}
@media (display-mode: standalone) and (prefers-color-scheme: darkish) {
header, footer {
background: #000;
}
header {
border-bottom: 1px strong #333;
}
}
You possibly can flip the header right into a sticky part by utilizing the place: sticky attribute. I normally favor to observe the iOS fashion when the web site is introduced to the end-user as a standalone app and I hold the unique theme colours for the net solely.
Do not forget to rebuild the backend server, earlier than you check your app. Since we have made some meta modifications you might need to delete the PWA bookmark and set up it once more to make issues work. ⚠️
As you possibly can see constructing handsome progressive internet apps for iOS is kind of tough, particularly if it involves the metadata insanity that Apple created. Anyway, I hope this tutorial will aid you to construct higher PWAs for the iOS platform. That is simply the tip of the iceberg, we have not talked about JavaScript in any respect, however perhaps I am going to come again with that subject in a brand new tutorial afterward.