• The missing ☑️: SwiftWebUI
  • The Always Right Institute
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: EmilyQiRabbit
  • Proofreader: iWeslie, Pingren

SwiftUI: SwiftWebUI on the Web terminal

Apple announced SwiftUI at WWDC 2019 earlier this month. It is a standalone “cross-platform” and “declarative” framework for building user interfaces (UIs) for tvOS, macOS, watchOS, and iOS. SwiftWebUI is migrating the framework to Web development ✔️.

Disclaimer: SwiftWebUI is just a toy-level project! Do not use in production environments. It is recommended to learn SwiftUI and its inner workings.

SwiftWebUI

So what exactly can SwiftWebUI be used for? The answer is to use SwiftWebUI, which displays your SwiftUI View in your Web browser.

import SwiftWebUI

struct MainPage: View {@State var counter = 0
  
  func countUp(a) { counter += 1 }
  
  var body: some View {
    VStack {
      Text("🥑 🍞 #\(counter)")
        .padding(.all)
        .background(.green, cornerRadius: 12)
        .foregroundColor(.white)
        .tapAction(self.countUp)
    }
  }
}
Copy the code

The code runs as follows:

Unlike the efforts of some other code libraries, it doesn’t just render SwiftUI Views as HTML. It also establishes a link between the browser and Swift server code to support user interaction — button, picker, Stepper, list, Navigation, etc., all supported.

In other words: SwiftWebUI is an implementation of the SwiftUI API in the browser (it implements most, but not all, of the API).

To reiterate the disclaimer: SwiftWebUI is just a toy project! Do not use in production environments. It is recommended to learn SwiftUI and its inner workings.

Once learned, everywhere used

SwiftUI’s core goal is not “code once, run everywhere”, but “learn once, use everywhere”. Don’t expect to take the beautiful SwiftUI app on iOS, copy the code into the SwiftWebUI project and see the exact same rendering in the browser. Because that’s not the point of SwiftWebUI.

The key is to be able to emulate SwiftUI by allowing developers to experiment with code and see results, as Knoff-Hoff does, while sharing across platforms. In this sense, the Web has an advantage.

Now let’s get down to the nitty gritty and write a simple SwiftWebUI application. In the spirit of “Learn once, use anywhere”, take a look at these two WWDC conference notes: SwiftUI Introduction and SwiftUI Core. Although we won’t go into depth in this blog post, we recommend you take a look at SwiftUI’s data flow (most of the concepts apply to SwiftWebUI as well).

The preparatory work needed

SwiftWebUI currently requires macOS Catalina to run due to the incompatibility of the Swift ABI. Fortunately, installing Catalina on a separate APFS bundle is simple. You will also need Xcode 11 installed in order to use the latest Swift 5.1 features, which SwiftUI will use heavily. Is that clear? Very good!

What if you’re running Linux? The project is almost ready to run on Linux, but the work is not yet complete. The missing part of the project is a simple implementation of Combine PassthroughSubject, and I’m having a little trouble with that. The currently prepared code is at: NoCombine. Welcome to pull Request for the project!

What if you use Mojave? There is a way to run projects on Mojave and Xcode 11. You need to create an iOS 13 emulator project and run the entire project in the emulator.

Start building your first application

Create SwiftWebUI project

Open Xcode 11 and choose File > New > Project… Or use the shortcut keys cmd-shift-n:

Select “macOS/Command Line Tool” project template:

To give the project an appropriate name, let’s call it “AvocadoToast” :

Then, add SwiftWebUI to Swift package manager and import the project. This option is in the ‘File/Swift Packages’ menu:

Enter the URL https://github.com/SwiftWebUI/SwiftWebUI.git as a package:

Set “Branch” to the master option so you can always get the latest and best code (you can also use revisions or use the Develop Branch) :

Finally, add SwiftWebUI library to target tool:

That will do. Now you have a tool project that directly imports SwiftWebUI. (Xcode may take a while to get and build dependencies.)

Use SwiftWebUI to display Hello World

Let’s start learning SwiftWebUI now. Open the main.swift file and replace it with:

import SwiftWebUI

SwiftWebUI.serve(Text("Holy Cow!"))
Copy the code

To compile the code and run the application in Xcode, open Safari and visit http://localhost:1337/ :

First the SwiftWebUI module is referenced (please be careful not to accidentally reference macOS SwiftUI 😀).

Next we call SwiftWebUI. Serve, which may use a closure that returns a View, or just a View — as shown above, the return is a Text View (aka “UILabel”, which can display simple or formatted Text).

The principle behind the scenes

The serve function creates a very simple SwiftNIO HTTP server that listens on port 1337. When the browser accesses the server, it creates a session and passes our (Text) View to that session. Finally SwiftWebUI creates a “Shadow DOM” in the server, renders the View as HTML and sends the results to the browser. This “Shadow DOM” (and a state object that is bound to it) is stored in the session.

The SwiftWebUI app is different from watchOS or the SwiftUI app on iOS. A SwiftWebUI app can serve multiple users, rather than just one, like the SwiftUI app.

Add some user interaction

After the first step, let’s optimize the structure of the code. Create a new Swift file in the project and name it ‘mainPage.swift. And add a simple SwiftUI View definition to it:

import SwiftWebUI

struct MainPage: View {
  
  var body: some View {
    Text("Holy Cow!")}}Copy the code

Adjust main.swift to serve our custom View:

SwiftWebUI.serve(MainPage())
Copy the code

Now we can leave main.swift behind and do the rest of the work in our custom View. Now let’s add some user interaction to it:

struct MainPage: View {@State var counter = 3
  
  func countUp(a) { counter += 1 }
  
  var body: some View {
    Text("Count is: \(counter)")
      .tapAction(self.countUp)
  }
}
Copy the code

Our View has a State variable called counter. I suggest you take a look at SwiftUI introduction). And a simple function to increment counter. We then bind the time handler function to our Text using the SwiftUI modifier tapAction. Finally, we display the current value in the tag:

🧙♀️ is like magic 🧙

The principle behind the scenes

How does it all work? When we click the browser, SwiftWebUI creates a session with “Shadow DOM” in it. It will then send the HTML description of the View to the browser. TapAction can be called to execute the onclick event handler added via HTML. SwiftWebUI can also transfer JavaScript code to the browser (only small amounts of code, not large frame code!). This part of the code will handle the click event and forward it to our Swift server.

Then it was the turn of SwiftUI magic. SwiftWebUI associates the click event with our event handler in Shadow DOM and calls the countUp function. The function invalidates the rendering of the View by modifying the variable counter State. SwiftWebUI then starts to compare the differences and changes in “Shadow DOM”. These changes will then be sent back to the browser.

These changes are sent as JSON arrays that can be parsed by a slice of JavaScript code on the page. If everything in a subtree of the HTML structure changes (for example, if the user enters a new View), then the change could be a larger slice of HTML code that will be applied to the innerHTML or outerHTML methods. Usually, however, the changes are minor, such as add class, set HTML attribute ‘(i.e. browser DOM modification).

🥑🍞 Avocado Toast application

Great. Now we’ve finished all the groundwork. Let’s add some more interactivity. The following is based on the Avocado Toast app, which was used as a SwiftUI model during the SwiftUI Core talk. If you haven’t seen it yet, I suggest you do. After all, it’s all about delicious toast.

HTML and CSS styles are not perfect or aesthetically pleasing. And as you know, we’re not Web designers, so we need your help with that. Welcome to make a pull Request for the project!

If you want to skip the details and check out a GIF of the app, you can download it on GitHub: 🥑🍞.

🥑🍞 Application Order Form

Let’s start with this code (which is about 6 minutes old in the video). First we’ll write it to a new orderForm.swift file:

struct Order {
  var includeSalt            = false
  var includeRedPepperFlakes = false
  var quantity               = 0
}
struct OrderForm: View {@State private var order = Order(a)func submitOrder(a) {}
  
  var body: some View {
    VStack {
      Text("Avocado Toast").font(.title)
      
      Toggle(isOn: $order.includeSalt) {
        Text("Include Salt")}Toggle(isOn: $order.includeRedPepperFlakes) {
        Text("Include Red Pepper Flakes")}Stepper(value: $order.quantity, in: 1.10) {
        Text("Quantity: \(order.quantity)")}Button(action: submitOrder) {
        Text("Order")}}}}Copy the code

This directly tests SwiftWebUI. Serve () in main.swift as well as the new OrderForm ‘View.

Here’s what it looks like in the browser:

SemanticUI can be used to define styles for some content in SwiftWebUI. It’s not necessary for the operation logic, but it can help you make widgets that look good. Note: it only uses CSS/ Fonts, not JavaScript components.

Relax: Get to know some SwiftUI layouts

Around the 16th minute of the SwiftUI Core talk, they started explaining the SwiftUI layout and View decorator order:

var body: some View {
  HStack {
    Text("🥑 🍞")
      .background(.green, cornerRadius: 12)
      .padding(.all)
    
    Text("= >")
    
    Text("🥑 🍞")
      .padding(.all)
      .background(.green, cornerRadius: 12)}}Copy the code

As a result, notice how the decorator order relates to each other:

SwiftWebUI has been trying to replicate some of the commonly used SwiftUI layouts, but has not been completely successful. After all, the work is related to the browser’s layout system. We need help, especially flexbox layout experts!

🥑🍞 Application history orders

Moving on to the introduction of the app, the presentation at about 19 minutes and 50 seconds introduced a List View that can be used to display the Avocado Toast app’s history of orders. Here’s what it looks like on the Web side:

The List View iterates through the array containing all orders, then creates a child View (OrderCell) for each item, passing information about each order in the List to this OrderCell.

Here’s the code we used:

struct OrderHistory: View {
  let previousOrders : [ CompletedOrder ]
  
  var body: some View {
    List(previousOrders) { order in
      OrderCell(order: order)
    }
  }
}

struct OrderCell: View {
  let order : CompletedOrder
  
  var body: some View {
    HStack {
      VStack(alignment: .leading) {
        Text(order.summary)
        Text(order.purchaseDate)
          .font(.subheadline)
          .foregroundColor(.secondary)
      }
      Spacer(a)if order.includeSalt {
        SaltIcon()}else {}
      if order.includeRedPepperFlakes {
        RedPepperFlakesIcon()}else{}}}}struct SaltIcon: View {
  let body = Text("🧂")}struct RedPepperFlakesIcon: View {
  let body = Text("🌶")}// Model

struct CompletedOrder: Identifiable {
  var id           : Int
  var summary      : String
  var purchaseDate : String
  var includeSalt            = false
  var includeRedPepperFlakes = false
}
Copy the code

The SwiftWebUI List View is extremely inefficient and always renders the entire collection of child elements. Cells (list cells) are not reused at all 😎. In Web applications, there are many different ways to solve this problem, for example, by using paging or using more client-side logic.

We’ve prepared the sample data code for you to use in the talk, so you don’t need to type it again:

let previousOrders : [ CompletedOrder] = [.init(id:  1, summary: "Rye with Almond Butter",  purchaseDate: "2019-05-30").init(id:  2, summary: "Multi-Grain with Hummus", purchaseDate: "2019-06-02",
        includeRedPepperFlakes: true).init(id:  3, summary: "Sourdough with Chutney",  purchaseDate: "2019-06-08",
        includeSalt: true, includeRedPepperFlakes: true).init(id:  4, summary: "Rye with Peanut Butter",  purchaseDate: "2019-06-09").init(id:  5, summary: "Wheat with Tapenade",     purchaseDate: "2019-06-12").init(id:  6, summary: "Sourdough with Vegemite", purchaseDate: "2019-06-14",
        includeSalt: true).init(id:  7, summary: "Wheat with Féroce",       purchaseDate: "2019-06-31").init(id:  8, summary: "Rhy with Honey",          purchaseDate: "2019-07-03").init(id:  9, summary: "Multigrain Toast",        purchaseDate: "2019-07-04",
        includeSalt: true).init(id: 10, summary: "Sourdough with Chutney",  purchaseDate: "2019-07-06")]Copy the code

🥑🍞 Application Spread Picker (deployable selector)

Picker control and how to use it with enumerated types is explained in about 43 minutes. First let’s look at the enumeration types of the different Toast popover options;

enum AvocadoStyle {
  case sliced, mashed
}

enum BreadType: CaseIterable.Hashable.Identifiable {
  case wheat, white, rhy
  
  var name: String { return "\ [self)".capitalized }
}

enum Spread: CaseIterable.Hashable.Identifiable {
  case none, almondButter, peanutButter, honey
  case almou, tapenade, hummus, mayonnaise
  case kyopolou, adjvar, pindjur
  case vegemite, chutney, cannedCheese, feroce
  case kartoffelkase, tartarSauce

  var name: String {
    return "\ [self)".map{$0.isUppercase ? " \ [$0)" : "\ [$0)" }
           .joined().capitalized
  }
}
Copy the code

We can add all of these to our Order structure:

struct Order {
  var includeSalt            = false
  var includeRedPepperFlakes = false
  var quantity               = 0
  var avocadoStyle           = AvocadoStyle.sliced
  var spread                 = Spread.none
  var breadType              = BreadType.wheat
}

Copy the code

Then display them using different types of pickers. You can easily loop through all the values of an enumerated type directly:

Form {
  Section(header: Text("Avocado Toast").font(.title)) {
    Picker(selection: $order.breadType, label: Text("Bread")) {
      ForEach(BreadType.allCases) { breadType in
        Text(breadType.name).tag(breadType)
      }
    }
    .pickerStyle(.radioGroup)
    
    Picker(selection: $order.avocadoStyle, label: Text("Avocado")) {
      Text("Sliced").tag(AvocadoStyle.sliced)
      Text("Mashed").tag(AvocadoStyle.mashed)
    }
    .pickerStyle(.radioGroup)
    
    Picker(selection: $order.spread, label: Text("Spread")) {
      ForEach(Spread.allCases) { spread in
        Text(spread.name).tag(spread) // there is no .name?!}}}}Copy the code

The result of the code run:

Again, we need some CSS savvy to make the interface look better…

🥑🍞 final result of application

We’re still slightly different from the original SwiftUI interface, and we’re not quite finished yet. It doesn’t look perfect, but it can be used to demonstrate 😎

The finished application code can be viewed on GitHub: AvocadoToast.

HTML and SemanticUI

UIViewRepresentable equivalents in SwiftWebUI are used to generate native HTML code.

It provides two variables that the HTML will either print the string as-is or translate the content through HTML:

struct MyHTMLView: View {
  var body: some View {
    VStack {
      HTML("<blink>Blinken Lights</blink>")
      HTML(42 "1337" >, escape: true)}}}Copy the code

With this structure, you can basically build any HTML you want.

A slightly more advanced level, but also used in SwiftWebUI is HTMLContainer. For example, here’s how Stepper control is implemented:

var body: some View {
  HStack {
    HTMLContainer(classes: [ "ui"."icon"."buttons"."small" ]) {
      Button(self.decrement) {
        HTMLContainer("i", classes: [ "minus"."icon" ], body: {EmptyView()})}Button(self.increment) {
        HTMLContainer("i", classes: [ "plus"."icon" ], body: {EmptyView()})
      }
    }
    label
  }
}
Copy the code

HTMLContainer is a little more flexible; for example, if an element’s class, style, or attribute changes, it will generate a regular DOM change (instead of rerendering everything).

SemanticUI

SwiftWebUI also includes some SemanticUI controls pre-configured:

VStack {
  SUILabel(Image(systemName: "mail")) { Text("42")}HStack {
    SUILabel(Image(...). ) {Text("Joe")}... }HStack {
    SUILabel(Image(...). ) {Text("Joe")}... }HStack {
    SUILabel(Image(...). .Color("blue"), 
             detail: Text("Friend")) 
    {
      Text("Veronika")}... }}Copy the code

… The render result is:

Note that SwiftWebUI also supports some built-in icon library (SFSymbols) Image names (Image(systemName:) ‘). SemanticUI’s support for Font Awesome is the technical support behind it.

SwiftWebUI also includes SUISegment, SUIFlag and SUICard:

SUICards {
  SUICard(Image.unsplash(size: UXSize(width: 200, height: 200),
                         "Zebra"."Animal"),
          Text("Some Zebra"),
          meta: Text("Roaming the world since 1976"))
  {
    Text("A striped animal.")}SUICard(Image.unsplash(size: UXSize(width: 200, height: 200),
                         "Cow"."Animal"),
          Text("Some Cow"),
          meta: Text("Milk it"))
  {
    Text("Holy cow! .")}}Copy the code

… The render looks like this:

Adding such a View is a breeze. With WOComponent’s SwiftUI Views, everyone can quickly create complex and beautiful layouts.

Image. Unsplash will run according to the pictures on the http://source.unsplash.com API build request. Just pass it some request parameters, such as the size of the image you want and other configurable options. Note: This Unsplash service can be slow and unreliable at times.

conclusion

That’s all for this demo. I hope you like it! But to reiterate the disclaimer: SwiftWebUI is just a toy project! Do not use in production environments. It is recommended to learn SwiftUI and its inner workings.

But we think it’s a great entry level playtest and a valuable tool to learn the inner workings of SwiftUI.

Optional technical instructions

Here is a list of tips on different aspects of the technology. You can skip this, it’s not so interesting 😎

Issues

Our project has many Issues, some of which are on Github: Issues. You can also try to give us more issues.

This covers a lot of HTML layout stuff (for example, scrollViews sometimes don’t scroll), but there are also a lot of open questions about shapes (which might be easier to implement if you use SVG or CSS).

There is also a problem with the invalidity of if-ViewBuilder. It’s not known what caused it:

var body: some View {
  VStack {
    if a > b {
      SomeView()}// We also need an empty else statement: 'else {}' to compile.}}Copy the code

We need help, welcome to make pull request for us!

Comparison to the native SwiftUI

Our current implementation method is very simple and not efficient. The official version had to deal with high frequency state changes, all animations had to be at 60Hz frame rates, etc.

We are currently concentrating on getting the basics done, such as how states and bindings work, when and how views are updated, and so on. Many times the implementation could be wrong, but Apple forgot to send us the original code as part of Xcode 11.

WebSockets

We now use AJAX to connect the browser to the server. Using WebSockets offers even greater advantages:

  • Ensures the order in which events are run (AJAX requests are asynchronous and return in a variable order)
  • No user initialization is required, but DOM updates on the server side (for example timers, push)
  • Session expiration can be detected

This will make the presentation of the chat client easier.

Adding websockets to the project is actually quite simple, since events are already sent in JSON format. All we need is the shiM on the client side and the server side. This part is already implemented in Swift-Nio-IRc-webClient and just needs to be migrated to the project.

SPA

SwiftWebUI is currently a SPA (single-page application) project, tied to a state-enabled back-end service.

There are other ways to implement SPA, such as leaving the state tree unchanged when a user switches between pages in an application via a normal link. Also called WebObjects; -)

In general, this is a good choice if you want more complete control over DOM ID generation, link generation, routing, and so on. But in the end, users may have to give up the “learn once, use anywhere”, because SwiftUI’s behavior processing functions are often built around the fact that they are meant to capture arbitrary states.

Next we’ll see what the Swift based server framework does 👽

WASM

When we use the appropriate Swift WASM, all the code becomes much more practical. Let’s learn WASM!

WebIDs

Some SwiftUI Views, such as ForEach, require an Identifiable object, so that the ID can be an arbitrary Hashable value. But it doesn’t perform very well when used with the DOM, because we need string ids to identify nodes. It works by mapping ids to strings through a global map structure. This is not technically difficult (just a specific class reference issue).

Summary: For Web-side code, it is wise to use strings or numbers to identify items.

Form

The form received a lot of attention: Issue.

SemanticUI has a lot of nice form layouts. We might rewrite this part of the subtree. Still to be perfected.

WebObjects 6 for Swift

Wait and click on it again:

SwiftUI summary for 40S + users. pic.twitter.com/6cflN0OFon

— Helge Heß (@Helje5) June 7, 2019

With SwiftUI, Apple really gave us WebObjects 6 in Swift mode!

Next up: Direct To Web and Swift EOF (CoreData or ZeeQL).

Refer to the link

  • SwiftWebUI on GitHub
  • SwiftUI
    • Introducing SwiftUI (204)
    • SwiftUI Essentials (216)
    • Data Flow Through SwiftUI (226)
    • SwiftUI Framework API
  • SwiftObjects
  • SemanticUI
    • Font Awesome
    • SemanticUI Swift
  • SwiftNIO

Contact us

Hey, we hope you enjoyed this article and we welcome your feedback! Feedback can be sent to Twitter or @helje5 or @ar_institute. The Email address is [email protected]. Slack: Find us at SwiftDE, Swift-Server, Noze, ios-developers.

Written on 30 June 2019

If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.