Files
iOS/Sources/App/Servers/ServerSelectView.swift
Bruno Pantaleão Gonçalves 6e84ff4cb6 Migrate app from UIKit based app to SwiftUI (#4748)
<!-- Thank you for submitting a Pull Request and helping to improve Home
Assistant. Please complete the following sections to help the processing
and review of your changes. Please do not delete anything from this
template. -->

## Summary
<!-- Provide a brief summary of the changes you have made and most
importantly what they aim to achieve -->
This PR is a massive refactor of how the app handles UI presentation and
navigation, goin from the UIKit based apps style to SwiftUI.

## Screenshots
<!-- If this is a user-facing change not in the frontend, please include
screenshots in light and dark mode. -->

## Link to pull request in Documentation repository
<!-- Pull requests that add, change or remove functionality must have a
corresponding pull request in the Companion App Documentation repository
(https://github.com/home-assistant/companion.home-assistant). Please add
the number of this pull request after the "#" -->
Documentation: home-assistant/companion.home-assistant#

## Any other notes
<!-- If there is any other information of note, like if this Pull
Request is part of a bigger change, please include it here. -->

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 10:33:17 +02:00

167 lines
5.3 KiB
Swift

import Shared
import SwiftUI
/// The server picker shown when an action (e.g. a server-less deep link, or the "show servers" gesture)
/// needs the user to choose a server. Presented as a sheet by `ContainerView`; dismisses via the SwiftUI
/// environment.
struct ServerSelectView: View {
@Environment(\.dismiss) private var dismiss
let prompt: String?
let includeSettings: Bool
let selectAction: (Server) -> Void
var body: some View {
NavigationView {
List {
if let prompt {
Section {
Text(prompt)
.font(.body)
.frame(maxWidth: .infinity, alignment: .leading)
.multilineTextAlignment(.leading)
.padding(.horizontal)
.listRowBackground(Color.clear)
.listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0))
}
}
Section {
ForEach(Current.servers.all, id: \.identifier) { server in
ServerSelectViewRow(server: server) {
selectAction(server)
dismiss()
}
}
}
}
.modify { view in
if #available(iOS 17.0, *) {
view.listSectionSpacing(DesignSystem.Spaces.one)
} else {
view
}
}
.navigationViewStyle(.stack)
.navigationBarTitleDisplayMode(.inline)
.navigationTitle(L10n.ServersSelection.title)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
CloseButton {
dismiss()
}
}
ToolbarItem(placement: .primaryAction) {
if includeSettings {
SettingsButton(tint: Color.haPrimary) {
dismiss()
Current.sceneManager.webViewControllerPromise
.done { controller in
controller.showSettingsViewController()
}
}
}
}
}
}
}
}
struct ServerSelectViewRow: View {
@State private var userName: String = ""
@State private var profilePictureImage: UIImage?
@State private var selected = false
let server: Server
let action: () -> Void
var body: some View {
Button(action: {
action()
}, label: {
HStack(spacing: DesignSystem.Spaces.two) {
profilePicture
VStack {
Group {
Text(server.info.name)
.font(.headline)
.foregroundStyle(Color(uiColor: .label))
Text(userName)
.font(.caption)
.foregroundStyle(Color(uiColor: .secondaryLabel))
}
.frame(maxWidth: .infinity, alignment: .leading)
.multilineTextAlignment(.leading)
}
}
})
.tint(Color.haPrimary)
.onAppear {
updateSelectionIndicator()
loadUserNameAndProfilePicture()
}
}
private var profilePicture: some View {
ZStack(alignment: .bottomTrailing) {
Group {
if let profilePictureImage {
Image(uiImage: profilePictureImage)
.resizable()
} else {
ZStack {
Image(systemSymbol: .circleFill)
.resizable()
Text(String(userName.first ?? Character(" ")))
.foregroundStyle(.white)
.font(.body.bold())
}
}
}
.frame(width: 40, height: 40)
.clipShape(Circle())
.overlay(
Circle()
.stroke(Color.haPrimary, lineWidth: 2)
)
if selected {
Image(systemSymbol: .checkmarkCircleFill)
.foregroundStyle(.white, .haPrimary)
.offset(x: 5, y: 5)
}
}
}
private func updateSelectionIndicator() {
Current.sceneManager.webViewControllerPromise.done { controller in
selected = controller.server == server
}
}
private func loadUserNameAndProfilePicture() {
guard let api = Current.api(for: server) else { return }
api.currentUser { user in
userName = user?.name ?? ""
guard let user else { return }
api.profilePicture(for: user) { image in
profilePictureImage = image
}
}
}
}
#Preview("Servers") {
ServerSelectView(prompt: nil, includeSettings: true) { _ in }
}
#Preview("Servers with prompt") {
ServerSelectView(prompt: "Are you sure?", includeSettings: false) { _ in }
}
#Preview("Rows") {
List {
ServerSelectViewRow(server: ServerFixture.standard) {}
}
}