This step-by-step tutorial is the quickest way to see Peregrine in action for yourself on iOS. As a reminder, you can use the buttons above to switch platforms for this documentation.
In the interest of saving time and reducing complexity, this tutorial is short on words and also not that practical. You are encouraged to embellish the code and be playful! Learning should be fun. 🤓
Create an example app
Use XCode to create a throwaway iOS app. Make sure to select SwiftUI for interface. Review these requirements for more information.
Install Peregrine
The first step is to install the Peregrine iOS library.
- In XCode, click on your project in the Project Navigator.
- Under “General”, find “Frameworks, Libraries, and Embedded Content” and click the add button.
- Click “Add Other…” and select “Add Package Dependency…”
- Paste the following URL into the search bar.
https://github.com/peregrinejs/Peregrine-iOS
- Click “Add Package” and make sure the
Peregrine
library is added to your app target.
Want to use CocoaPods?
Peregrine primarily uses Swift Package
Manager, but you can also install it by
adding the Peregrine
pod to your Podfile
.
target 'MyApp' do
pod 'Peregrine', '~> 0.10'
end
Add web assets
Before we can use Peregrine, we need an index.html
file and any accompanying
web assets.
- In XCode, right click on your project in the Project Navigator and select “Show in Finder”.
- In the Finder window, create a new folder named
www
. This folder will serve as the root of the web assets. - Within that folder, create an
index.html
file and paste in the following contents.
<!DOCTYPE html>
<html dir="ltr" lang="en">
<head>
<meta charset="utf-8" />
<title>My App</title>
<meta
name="viewport"
content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
</head>
<body>
<p>Hello World!</p>
</body>
</html>
Instantiate a Web Frame
For this example, we’ll replace the “Hello World” view in the SwiftUI example
template with a Web Frame that will render our index.html
.
- In XCode, open
ContentView.swift
. This is your app’s root view. - Add a
frame
property to the struct. - Replace the example view in the
body
property withframe.view
. - Provide the
frame
property in the preview provider.
import SwiftUI
import Peregrine
struct ContentView: View {
let frame: WebFrame
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
}
.padding()
frame.view
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
ContentView(frame: WebFrame(configuration: WebFrame.Configuration()))
}
}
- Next, open
*App.swift
. This is your app’s entry point. It should be decorated with@main
. - Add an initializer that creates a Web Frame. Be sure to configure
baseURL
to point to thewww
folder in your bundle. - Finally, pass the Web Frame into your
ContentView
.
import SwiftUI
import Peregrine
@main
struct MyApp: App {
let frame: WebFrame
init() {
let baseURL = Bundle.main.url(forResource: "www", withExtension: nil)!
let configuration = WebFrame.Configuration(baseURL: baseURL)
frame = WebFrame(configuration: configuration)
}
var body: some Scene {
WindowGroup {
ContentView()
ContentView(frame: frame)
}
}
}
Run the app
Under Construction
Add native functionality
Let’s add some native functionality to our iOS app that we can invoke from our web content.
- In XCode, open
*App.swift
.
- Add a
ping
function that expects the string'ping'
to be sent as the request data—if so,'pong'
is sent back; otherwise, the request fails with a message and an error code.
import SwiftUI
import Peregrine
func ping(call: Call) {
if call.request.text == "ping" {
call.respond(with: "pong")
} else {
call.fail("Expected 'ping' in request.", code: "EXPECTED_PING")
}
}
@main
struct MyApp: App {
- Add the
functions
option to the Web Frame configuration and pass in theping
function under the'ping'
key. This key will be used by the web client. Read more about remote interfaces.
init() {
let baseURL = Bundle.main.url(forResource: "www", withExtension: nil)!
let configuration = WebFrame.Configuration(baseURL: baseURL)
let configuration = WebFrame.Configuration(
baseURL: baseURL,
functions: [
"ping": ping,
]
)
frame = WebFrame(configuration: configuration)
}
- Open
index.html
. - Add a script in
<head>
that imports theProxyClient
from the Peregrine web library and connect it to thewindow
instance.. - Add a button to the
<body>
with an ID ofpingButton
. - Attach a click event listener to the button by its ID. When clicked, we’ll
call the
ping
function and print the response to the screen. If the request is unsuccessful, we’ll log the error message and code.
content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<script type="module">
import { ProxyClient } from 'https://unpkg.com/@peregrine/web'
const client = new ProxyClient()
client.connect(window)
const btn = document.getElementById('pingButton')
btn.addEventListener('click', async () => {
try {
const response = await client.ping('ping')
document.body.append(response, document.createElement('p'))
} catch (error) {
console.error(`${error.code}: ${error.message}`)
}
})
</script>
</head>
<body>
<p>Hello World!</p>
<button id="pingButton">Ping!</button>
<br />
</body>
</html>
When we run the app again, we’ll see our button. Whenever the button is tapped, “pong” will be appended to the web view.
Congrats! 🎁
You’ve just built your first bit of cross-platform interaction powered by Peregrine. From here you have a few options:
- Try some debugging by adding breakpoints in XCode and Safari.
- Change
'ping'
to another string to see the error message and code we’ve defined. - Change
.ping()
to an unknown function name to see what happens. - Read about the architecture of Peregrine to learn more.
- Continue on to learn two other main features of the Web Frame: events and path handlers.
Add native events
Now that we’ve seen how we can implement native functionality, let’s add native events that propagate to the web layer. We’ll be implementing a new function that will initiate a mock download and report status events to the web layer.
We’ll be using the Combine framework to asynchronously send native events to the web layer. This official library powers reactive state in SwiftUI views and is a modern and declarative way to process values over time.
Combine doesn’t need to be installed—it is installed by default in SwiftUI apps.
- In XCode, open
*App.swift
. - Create a
CurrentValueSubject
which will report status events. - Create a
startDownload
function that imitates downloading a file and sends progress events via thestatus
subject. - Pass in the
startDownload
function to thefunctions
dictionary under the'startDownload'
key. - Add the
observables
option to the Web Frame configuration and pass in thestatus
publisher under the'downloadStatus$'
key, first converting it to anAnyPublisher
.
import Combine
import SwiftUI
import Peregrine
func ping(call: Call) {
if call.request.text == "ping" {
call.respond(with: "pong")
} else {
call.fail("Expected 'ping' in request.", code: "EXPECTED_PING")
}
}
let status = CurrentValueSubject<Event?, Never>(nil)
func startDownload(call: Call) {
var progress = 0
call.respond()
status.value = Event("pending")
DispatchQueue.main.async {
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { timer in
if progress >= 100 {
status.value = Event("progress: 100%")
status.value = Event("completed")
timer.invalidate()
} else {
status.value = Event("progress: \(progress)%")
progress += 25
}
}
}
}
@main
struct MyApp: App {
let frame: WebFrame
init() {
let baseURL = Bundle.main.url(forResource: "www", withExtension: nil)!
let configuration = WebFrame.Configuration(
baseURL: baseURL,
functions: [
"ping": ping,
"startDownload": startDownload,
],
observables: [
"downloadStatus$": status.eraseToAnyPublisher(),
]
)
- Open
index.html
. - Add a button next to the ping button with the ID of
downloadButton
. - Attach a click event listener to the button by its ID. When clicked, we’ll
call the
startDownload
function to initiate the mock download. - Create a
listenForStatus
function which asynchronously iterates over status events from thedownloadStatus$
observable and prints them to the screen. - Call
listenForStatus
immediately to begin listening.
}
})
const downloadBtn = document.getElementById('downloadButton')
downloadBtn.addEventListener('click', () => {
client.startDownload()
})
const listenForStatus = async () => {
for await (const status of client.downloadStatus$) {
document.body.append(status, document.createElement('p'))
}
}
listenForStatus()
</script>
</head>
<body>
<p>Hello World!</p>
<button id="pingButton">Ping!</button>
<button id="downloadButton">Download</button>
<br />
</body>
When we run the app again, we’ll see our new download button. When the button is tapped, the mock download statuses will be appended to the web view.