Home IOS Development Progressive Net Apps on iOS

Progressive Net Apps on iOS

0
Progressive Net Apps on iOS

[ad_1]

Easy methods to make a PWA for iOS?

A progressive internet utility is only a particular form of web site, that may look and behave like a local iOS app. With a purpose to construct a PWA, first we’ll create a daily web site utilizing SwiftHtml. We are able to begin with a daily executable Swift bundle with the next dependencies.


import PackageDescription

let bundle = Bundle(
    identify: "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(identify: "ExampleTests", dependencies: ["Example"]),
    ]
)

As you may see we’ll use the vapor Vapor library to serve our HTML website. If you do not know a lot about Vapor let’s simply say that it’s a internet utility framework, which can be utilized to construct server facet Swift functions, it is a fairly wonderful instrument I’ve a newbie’s information submit about it.

In fact we’ll want some elements for rendering views utilizing SwiftHtml, you should use the supply snippets from my earlier article, however right here it’s once more how the SwiftHtml-based template engine ought to appear to be. You must 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 important HTML doc. Since we’re utilizing a Swift DSL to jot down HTML code we do not have to fret an excessive amount of about mistyping a tag, the compiler will shield us and helps to take care of a totally 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().identify(.viewport).content material("width=device-width, initial-scale=1")
            }
            Physique {
                Major {
                    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 through the use of 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 = Software(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 straightforward to setup and bootstrap a totally working internet server that’s able to rendering a HTML doc utilizing the ability of Swift and the Vapor framework. In case you run the app you must have the ability to see a working web site by visiting the http://localhost:8080/ deal with.

Turning an internet site into an actual iOS PWA

Now if we wish to remodel our web site right into a standalone PWA, we’ve to offer a hyperlink a particular internet app manifest file inside the pinnacle part of the index template.

Meta tags vs manifest.json

Looks like Apple follows form of a wierd route if it involves PWA assist. 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 inclined to comply with the requirements in any respect. For Android gadgets you can create a manifest.json file with some predefined keys and you would be simply high quality together with your PWA. Alternatively Apple these days prefers varied HTML meta tags as an alternative of the net manifest format.

Personally I do not like this method, as a result of your HTML code can be bloated with all of the PWA associated stuff (as you will see that is going to occur if it involves launch display screen photographs) and I consider it is higher to separate these form of issues, however hey it is Apple, they cannot be mistaken, proper? 😅

Anyhow, let me present you learn how to assist varied PWA options on iOS.

Enabling standalone app mode

The very first few keys that we would like so as to add to the index template has the apple-mobile-web-app-capable identify and you must 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 should be displayed utilizing Safari identical to a daily website.

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().identify(.viewport).content material("width=device-width, initial-scale=1")

                Meta()
                    .identify(.appleMobileWebAppCapable)
                    .content material("sure")
            }
            Physique {
                Major {
                    Div {
                        H1(context.title)
                        P(context.message)
                    }
                }
            }
        }
    }
}

We should always change the hostname of the server and hear on the 0.0.0.0 deal with, this manner in case your cellphone is on the identical native WiFi community you must have the ability to attain your internet server instantly.

import Vapor
import SwiftHtml

var env = strive Surroundings.detect()
strive LoggingSystem.bootstrap(from: &env)
let app = Software(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 deal with 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 deal with and go to the http://[ip-address]:8080/ web site utilizing your iOS machine, then you must have the ability to add your web site to your property display screen as a bookmark. Simply faucet the Share icon utilizing Safari and choose the Add to Residence Display screen menu merchandise from the listing. On the brand new display screen faucet the Add button on the highest proper nook, this may create a brand new icon on your property display screen as a bookmark to your web page. Optionally, you may present a customized identify for the bookmark. ☺️

Since we have added the meta tag, in case you contact the newly created icon it ought to open the webpage as a standalone app (with out utilizing the browser). In fact the app continues to be only a web site rendered utilizing an internet view. The standing bar will not match the white background and it has no customized icon or splash display screen but, however we’ll repair these points proper now. 📱

Customized identify and icon

To supply a customized identify we simply have so as to add a brand new meta tag, luckily the SwiftHtml library has predefined enums for all of the Apple associated meta names, so you do not have to kind that a lot. The icon state of affairs is a little more troublesome, since we’ve 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().identify(.viewport).content material("width=device-width, initial-scale=1")
                
                Meta()
                    .identify(.appleMobileWebAppCapable)
                    .content material("sure")
                
                Meta()
                    .identify(.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 {
                Major {
                    Div {
                        H1(context.title)
                        P(context.message)
                    }
                }
            }
        }
    }
}

As you may see icons are referenced through the use of the Hyperlink tag, utilizing the Apple contact icon rel attribute. The default icon with out the sizes attribute is usually a 192×192 pixel picture, plus I am offering some smaller sizes through the use of a for loop right here. We additionally have to serve these icon recordsdata through the use of Vapor, that is why we’ll alter the configuration file and allow the FileFiddleware.

import Vapor
import SwiftHtml

var env = strive Surroundings.detect()
strive LoggingSystem.bootstrap(from: &env)
let app = Software(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 happy to create it and place the app icons beneath the Public/img/apple/icons folder. If you’re working the server from the command line you will be high quality, however if you’re utilizing Xcode it’s important to specify a customized working listing for Vapor, this may enable the system to search for the general public recordsdata from the precise 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. If you see the add bookmark web page you must have the ability to validate that the app now makes use of the predefined Howdy PWA identify and the picture preview ought to present the customized icon file as an alternative of a screenshot of the web page.

Correct standing bar coloration for iOS PWAs

Lengthy story brief, there’s a nice article on CSS-Tips about the latest model of Safari and the way it handles varied theme colours on completely different platforms. It is a terrific article, you must positively learn it, however in many of the circumstances you will not want this a lot data, however you merely wish to assist mild and darkish mode in your progressive internet app. That is what I will present you right here.

For mild mode we’ll use a white background coloration 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 coloration based on the present coloration scheme. First, the brand new meta tags to assist theme colours each for mild 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().identify(.viewport).content material("width=device-width, initial-scale=1")
                
                Meta()
                    .identify(.appleMobileWebAppCapable)
                    .content material("sure")
                Meta()
                    .identify(.appleMobileWebAppTitle)
                    .content material("Howdy PWA")
                
                Meta()
                    .identify(.colorScheme)
                    .content material("mild darkish")
                Meta()
                    .identify(.themeColor)
                    .content material("#fff")
                    .media(.prefersColorScheme(.mild))
                Meta()
                    .identify(.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 {
                Major {
                    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 coloration scheme, identical to we did it for the .themeColor meta tag utilizing SwiftHtml.

physique {
    background: #fff;
    coloration: #000;
}
@media (prefers-color-scheme: darkish) {
    physique {
        background: #000;
        coloration: #fff;
    }
}

That is it, now the standing bar ought to use the identical coloration as your important background. Attempt to swap between darkish and lightweight mode and ensure every thing works, there’s a cool PWA demo venture right here with completely different colours for every mode if you wish to double examine the code. ✅

Splash display screen assist

Trace: it is ridiculous. Splash screens on iOS are problematic. Even native apps are inclined to cache the mistaken splash display screen or will not render PNG recordsdata correctly, now if it involves PWAs this is not obligatory higher. I used to be in a position to present splash display screen photographs for my app, but it surely took me fairly some time and switching between darkish and lightweight mode is completely damaged (so far as I do know it). 😅

With a purpose to cowl each single machine display screen measurement, it’s important to add a lot of linked splash photographs 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,
                _ peak: Int,
                _ ratio: Int,
                _ orientation: MediaQuery.Orientation) -> Tag {
        splashTag(.mild, width, peak, ratio, orientation)
        splashTag(.darkish, width, peak, ratio, orientation)
    }
        
    func splashTag(_ mode: MediaQuery.ColorScheme,
                   _ width: Int,
                   _ peak: 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, peak, ratio, orientation))(mode == .mild ? "" : "_dark").png")
    }
    
    func calc(_ width: Int,
              _ peak: Int,
              _ ratio: Int,
              _ orientation: MediaQuery.Orientation) -> String {
        let w = String(width * ratio)
        let h = String(peak * ratio)
        swap 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 unsure if the result’s one thing I can completely agree with. Right here, check out the tip of this tutorial about splash screens, the code required to assist iOS splash screens could be very lengthy and I have never even informed you in regards to the 40 completely different picture recordsdata that you will want. Individuals are actually utilizing PWA asset turbines to cut back the time wanted to generate these form of footage, 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 assist and the notch. I can extremely suggest to learn this text on CSS-Tips 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 we’ve to alter the viewport meta tag and lengthen our web page past the secure space. This may be finished through the use of the viewport-fit cowl worth. Contained in the physique of the template we’ll add a header and a footer part, these areas may 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()
                    .identify(.viewport)
                    .content material("width=device-width, initial-scale=1, viewport-fit=cowl")
                    
                
                Meta()
                    .identify(.appleMobileWebAppCapable)
                    .content material("sure")
                Meta()
                    .identify(.appleMobileWebAppTitle)
                    .content material("Howdy PWA")
                
                Meta()
                    .identify(.colorScheme)
                    .content material("mild darkish")
                Meta()
                    .identify(.themeColor)
                    .content material("#fff")
                    .media(.prefersColorScheme(.mild))
                Meta()
                    .identify(.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")
                }
                
                Major {
                    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 coloration 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 primarily based on the atmosphere. Additionally we can safely use the calc CSS operate if we wish to add some additional worth to the atmosphere.

* {
    margin: 0;
    padding: 0;
}
physique {
    background: #fff;
    coloration: #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;
        coloration: #fff;
    }
    header {
        background: #222;
    }
    footer {
        background: #222;
    }
}

It seems 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 we’ve 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 examine if the app runs in a standalone mode you will should create an internet 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 should use the display-mode selector to examine if the app is working in a standalone mode, you may 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;
        high: 0;
        border-bottom: 1px stable #eee;
    }
}
@media (display-mode: standalone) and (prefers-color-scheme: darkish) {
    header, footer {
        background: #000;
    }
    header {
        border-bottom: 1px stable #333;
    }
}

You’ll be able to flip the header right into a sticky part through the use of the place: sticky attribute. I normally choose to comply with the iOS fashion when the web site is offered 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 may see constructing handsome progressive internet apps for iOS is kind of difficult, particularly if it involves the metadata insanity that Apple created. Anyway, I hope this tutorial will show you how to 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 will come again with that subject in a brand new tutorial afterward.

[ad_2]

LEAVE A REPLY

Please enter your comment!
Please enter your name here