Skip to content

Instantly share code, notes, and snippets.

@markmals
Last active February 7, 2025 00:43
Show Gist options
  • Save markmals/bd05392fdb65e457a9aaad0e27533a83 to your computer and use it in GitHub Desktop.
Save markmals/bd05392fdb65e457a9aaad0e27533a83 to your computer and use it in GitHub Desktop.
An exploration of "server views" (server components) in Swift with a SwiftUI-like API
import SwiftData
import SwiftUI
import Vapor
let container = try! ModelContainer(for: [Message.self, User.self])
let context = ModelContext(container)
@ClientView
struct Counter: View {
@State var count = 0
var body: some View {
Text(count)
Button(action: { count += 1 }) {
Text("Increment")
}
}
}
@ServerFunc(cache: true)
func fetchMessage(for id: Int) async -> Message {
let message = try? await context.fetch(
FetchDescriptor<Message>(
predicate: #Predicate { $0.id == id }
)
).first
return message ?? Message(description: "Message not found")
}
@ClientView
struct MessageClient: View {
let id: Int
@State var message: Message? = nil
var body: some View {
Group {
Counter()
if let message {
Text(message.localizedDescription)
} else {
ProgressView()
}
}
.task { message = try? await fetchMessage(for: id) }
}
}
struct MessageServer: View {
let message: Message
init() async {
message = try! await fetchMessage(for: id)
}
var body: some View {
// The code for this view will be sent to the client
Counter()
// This view will be serialized and statically rendered
// with the initial values from the initial server load.
// The code for this view will never be sent to the client.
Text(message.localizedDescription)
}
}
@ServerFunc
func echo(message: String) async throws -> String {
try await Task.sleep(for: .seconds(1))
return message
}
@ClientView
struct Echo: View {
let state = ActionState(action: echo, defaultValue: "Awaiting Message")
@State var inputText = ""
var body: some View {
TextField(text: $inputText)
.onSubmit(of: $inputText) { text in
Task { try? await state.action(text) }
}
if !state.isPending {
Text(state.value)
}
}
}
struct ErrorBanner: View {
let text: String
var body: some View {
RoundedRectangle()
.foregroundStyle(BannerStyle(.error))
.overlay { Text("Error: \(text)") }
}
}
@ServerFunc
func login(_ username: String) async throws -> some View {
let users = try await context.fetch(
FetchDescriptor<User>(
predicate: #Predicate { $0.username == (username ?? "") }
)
)
if username == "admin" {
throw Response.redirect("/admin")
}
if users?.count > 0 {
throw Response.redirect("/account/\(username)")
}
return ErrorBanner(text: "Invalid Username")
}
@ClientView
struct LoginForm: View {
let state = ActionState(action: login, defaultValue: EmptyView())
@State var username = ""
var body: some View {
Form {
TextField("Username", text: $username)
Button(action: submitForm) { Text("Log in") }
.disabled(state.isPending)
if !state.isPending {
state.value
}
}
.onSubmit { submitForm() }
}
private func submitForm() {
Task { try? await state.action(username) }
}
}
struct HomeController: RouteCollection {
func boot(routes: RoutesBuilder) {
routes.get("/", ":id", ":brand", use: index)
}
func index(request: Request) async throws -> some View {
let id = request.parameters.get("id", as: Int.self)!
let brand = request.parameters.get("brand")
// Preload cached data at the route level before fetching within views
try? await message(id: id)
return await request.ui.render {
Group {
if let brand {
Text("Welcome to \(brand)")
} else {
Text("Welcome Home!")
}
await MessageServer(id: id)
MessageClient(id: id)
}
.font(.title)
Text("Log In").font(.title2)
LoginForm()
} onError: {
Text("Received error: \($0.localizedDescription)")
}
}
}
let app = Application()
try app.register(collection: HomeController())
try app.start()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment