Last active
February 7, 2025 00:43
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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