Skip to content

Commit c42ae1d

Browse files
bgoncalCopilot
andauthored
Improved welcome and server list onboarding (#3617)
<!-- 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 --> ## Screenshots <!-- If this is a user-facing change not in the frontend, please include screenshots in light and dark mode. --> https://github.com/user-attachments/assets/3c38cf4d-94dc-479e-9475-dbbae9080540 ## 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: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 87a017c commit c42ae1d

File tree

9 files changed

+139
-59
lines changed

9 files changed

+139
-59
lines changed

HomeAssistant.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -757,6 +757,7 @@
757757
4286260D2DA5CD1B00D58D13 /* CornerRadiusSizesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 428626082DA5CD1B00D58D13 /* CornerRadiusSizesTests.swift */; };
758758
4286260E2DA5CD1B00D58D13 /* WebhookSensorIdTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4286260B2DA5CD1B00D58D13 /* WebhookSensorIdTests.swift */; };
759759
4286260F2DA5CD1B00D58D13 /* SpacesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 428626092DA5CD1B00D58D13 /* SpacesTests.swift */; };
760+
42881BD42DDF12340079BDCB /* SwiftUI+SafeArea.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42881BD32DDF12340079BDCB /* SwiftUI+SafeArea.swift */; };
760761
428830EB2C6E3A8D0012373D /* WatchHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 428830EA2C6E3A8D0012373D /* WatchHomeView.swift */; };
761762
428830ED2C6E3A9A0012373D /* WatchHomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 428830EC2C6E3A9A0012373D /* WatchHomeViewModel.swift */; };
762763
4289DDAA2C85AB4C003591C2 /* AssistAppIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 425FF0552C8216B3000AA641 /* AssistAppIntent.swift */; };
@@ -2220,6 +2221,7 @@
22202221
428626082DA5CD1B00D58D13 /* CornerRadiusSizesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CornerRadiusSizesTests.swift; sourceTree = "<group>"; };
22212222
428626092DA5CD1B00D58D13 /* SpacesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpacesTests.swift; sourceTree = "<group>"; };
22222223
4286260B2DA5CD1B00D58D13 /* WebhookSensorIdTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebhookSensorIdTests.swift; sourceTree = "<group>"; };
2224+
42881BD32DDF12340079BDCB /* SwiftUI+SafeArea.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftUI+SafeArea.swift"; sourceTree = "<group>"; };
22232225
428830EA2C6E3A8D0012373D /* WatchHomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchHomeView.swift; sourceTree = "<group>"; };
22242226
428830EC2C6E3A9A0012373D /* WatchHomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchHomeViewModel.swift; sourceTree = "<group>"; };
22252227
4289DDAE2C85D5C4003591C2 /* ControlScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlScene.swift; sourceTree = "<group>"; };
@@ -3789,6 +3791,7 @@
37893791
39A32EE12C0E384E00985722 /* UIImage+scaledToSize.swift */,
37903792
42AA4C832C2DACAD00EA2E99 /* UIImage+Circle.swift */,
37913793
117D8A0724A9347F00580913 /* UIColor+CSSRGB.swift */,
3794+
42881BD32DDF12340079BDCB /* SwiftUI+SafeArea.swift */,
37923795
);
37933796
path = Extensions;
37943797
sourceTree = "<group>";
@@ -7720,6 +7723,7 @@
77207723
46CC96822D7136FF00F784CA /* Array+SafeSubscripting.swift in Sources */,
77217724
429BEA1A2D102F3A00F070F9 /* ConnectionErrorDetailsView.swift in Sources */,
77227725
11EFCDD624F5FA8D00314D85 /* WebViewSceneDelegate.swift in Sources */,
7726+
42881BD42DDF12340079BDCB /* SwiftUI+SafeArea.swift in Sources */,
77237727
42B94BDF2B9606CD00DEE060 /* AssistView.swift in Sources */,
77247728
1185DF94271FBA6100ED7D9A /* OnboardingAuthDetails.swift in Sources */,
77257729
4273C48B2C8858470065A5B4 /* ControlOpenPageValueProvider.swift in Sources */,

Sources/App/Onboarding/Screens/OnboardingServersList/OnboardingServersListView.swift

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ struct OnboardingServersListView: View {
1212
@State private var showDocumentation = false
1313
@State private var showManualInput = false
1414
@State private var screenLoaded = false
15+
@State private var showHeaderView = false
16+
@State private var showManualInputButton = false
1517

1618
let prefillURL: URL?
1719

@@ -29,16 +31,21 @@ struct OnboardingServersListView: View {
2931
} else {
3032
if let inviteURL = Current.appSessionValues.inviteURL {
3133
prefillURLHeader(url: inviteURL)
32-
Text("Other options")
34+
Text(L10n.Onboarding.Invitation.otherOptions)
3335
.frame(maxWidth: .infinity, alignment: .center)
3436
.multilineTextAlignment(.center)
3537
.foregroundStyle(.secondary)
3638
.listRowBackground(Color.clear)
3739
} else {
38-
headerView
40+
if showHeaderView, viewModel.discoveredInstances.isEmpty {
41+
headerView
42+
}
3943
}
4044
list
41-
manualInputButton
45+
46+
if showManualInputButton {
47+
manualInputButton
48+
}
4249
}
4350
}
4451
.animation(.easeInOut, value: viewModel.discoveredInstances.count)
@@ -113,6 +120,15 @@ struct OnboardingServersListView: View {
113120
viewModel.startDiscovery()
114121
}
115122
}
123+
124+
// Only displays magnifying glass animation if no servers are found after 1.5 seconds
125+
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
126+
if viewModel.discoveredInstances.isEmpty {
127+
showHeaderView = true
128+
}
129+
130+
showManualInputButton = true
131+
}
116132
}
117133

118134
private func onDisappear() {

Sources/App/Onboarding/Screens/OnboardingWelcomeView.swift

Lines changed: 56 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,59 +2,98 @@ import Foundation
22
import Shared
33
import SwiftUI
44

5+
private enum OnboardingWelcomeConstants {
6+
static let logoWidth: CGFloat = 147
7+
static let logoHeight: CGFloat = 174
8+
static let logoOffsetDelay: Double = 0.3
9+
static let logoAnimationDuration: Double = 0.5
10+
static let textBlockDelay: Double = 0.9
11+
static let textBlockAnimationDuration: Double = 0.5
12+
static let textBlockYOffset: CGFloat = 320
13+
static let logoMaxWidth: CGFloat = 300
14+
static let logoVerticalPadding: CGFloat = Spaces.four
15+
static let continueButtonHorizontalPadding: CGFloat = Spaces.two
16+
static let continueButtonMinHeight: CGFloat = 40
17+
}
18+
519
struct OnboardingWelcomeView: View {
20+
@Environment(\.safeAreaInsets) private var safeAreaInsets
621
@State private var showLearnMore = false
7-
22+
@State private var animateLogo = false
23+
@State private var showText = false
824
@Binding var shouldDismissOnboarding: Bool
925

1026
var body: some View {
11-
VStack(spacing: .zero) {
12-
Spacer()
13-
Group {
14-
logoBlock
15-
textBlock
27+
ZStack(alignment: animateLogo ? .top : .center) {
28+
logoBlock
29+
.offset(x: 0, y: animateLogo ? safeAreaInsets.top + Spaces.five : 0)
30+
textBlock
31+
.offset(x: 0, y: OnboardingWelcomeConstants.textBlockYOffset)
32+
.opacity(showText ? 1 : 0)
33+
VStack {
34+
Spacer()
35+
continueButtonBlock
36+
.padding(.bottom, safeAreaInsets.bottom)
1637
}
17-
Spacer()
18-
continueButton
1938
}
39+
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
40+
.ignoresSafeArea()
2041
.frame(maxWidth: Sizes.maxWidthForLargerScreens)
42+
.onAppear {
43+
DispatchQueue.main.asyncAfter(deadline: .now() + OnboardingWelcomeConstants.logoOffsetDelay) {
44+
withAnimation(.easeInOut(duration: OnboardingWelcomeConstants.logoAnimationDuration)) {
45+
animateLogo = true
46+
}
47+
}
48+
49+
DispatchQueue.main.asyncAfter(deadline: .now() + OnboardingWelcomeConstants.textBlockDelay) {
50+
withAnimation(.easeInOut(duration: OnboardingWelcomeConstants.textBlockAnimationDuration)) {
51+
showText = true
52+
}
53+
}
54+
}
2155
.fullScreenCover(isPresented: $showLearnMore) {
2256
SafariWebView(url: AppConstants.WebURLs.homeAssistantGetStarted)
2357
}
2458
}
2559

2660
private var logoBlock: some View {
27-
Image(uiImage: Asset.logoHorizontalText.image)
61+
Image(.launchScreenLogo)
2862
.resizable()
2963
.aspectRatio(contentMode: .fit)
30-
.frame(maxWidth: 300)
31-
.padding(.vertical, Spaces.four)
64+
.frame(
65+
width: OnboardingWelcomeConstants.logoWidth,
66+
height: OnboardingWelcomeConstants.logoHeight,
67+
alignment: .center
68+
)
3269
}
3370

3471
private var textBlock: some View {
3572
ScrollView {
36-
VStack(spacing: Spaces.two) {
73+
VStack(alignment: .center, spacing: Spaces.two) {
3774
Text(verbatim: L10n.Onboarding.Welcome.description)
3875
.foregroundStyle(Color(uiColor: .secondaryLabel))
76+
.multilineTextAlignment(.center)
3977
}
4078
.padding()
4179
}
4280
}
4381

44-
private var continueButton: some View {
82+
private var continueButtonBlock: some View {
4583
VStack {
4684
NavigationLink(destination: OnboardingServersListView()) {
47-
Text(verbatim: L10n.continueLabel)
85+
Text(verbatim: L10n.Onboarding.Welcome.continueButton)
4886
}
4987
.buttonStyle(.primaryButton)
50-
.padding(.horizontal, Spaces.two)
51-
Button(L10n.Onboarding.Welcome.learnMore) {
88+
.padding(.horizontal, OnboardingWelcomeConstants.continueButtonHorizontalPadding)
89+
Button(L10n.Onboarding.Welcome.secondaryButton) {
5290
showLearnMore = true
5391
}
5492
.tint(Color.haPrimary)
55-
.frame(minHeight: 40)
93+
.frame(minHeight: OnboardingWelcomeConstants.continueButtonMinHeight)
5694
.buttonStyle(.secondaryButton)
5795
}
96+
.opacity(showText ? 1 : 0)
5897
}
5998
}
6099

Sources/App/Resources/Base.lproj/LaunchScreen.storyboard

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
<?xml version="1.0" encoding="UTF-8"?>
2-
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="22505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
2+
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23727" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
33
<device id="retina6_1" orientation="portrait" appearance="light"/>
44
<dependencies>
55
<deployment identifier="iOS"/>
6-
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22504"/>
6+
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23721"/>
77
<capability name="Named colors" minToolsVersion="9.0"/>
88
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
99
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@@ -17,24 +17,20 @@
1717
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
1818
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
1919
<subviews>
20-
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="1000" verticalHuggingPriority="1000" horizontalCompressionResistancePriority="250" verticalCompressionResistancePriority="250" misplaced="YES" image="launchScreen-logo" translatesAutoresizingMaskIntoConstraints="NO" id="mQh-tn-BC1" userLabel="Icon">
21-
<rect key="frame" x="134" y="361" width="147" height="174"/>
20+
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="1000" verticalHuggingPriority="1000" horizontalCompressionResistancePriority="250" verticalCompressionResistancePriority="250" image="launchScreen-logo" translatesAutoresizingMaskIntoConstraints="NO" id="mQh-tn-BC1" userLabel="Icon">
21+
<rect key="frame" x="133.5" y="361" width="147" height="174"/>
2222
<constraints>
23-
<constraint firstAttribute="width" constant="256" id="Qth-YB-gY0"/>
24-
<constraint firstAttribute="height" constant="256" id="h4K-N1-xO4"/>
23+
<constraint firstAttribute="width" constant="147" id="xXY-zT-zVi"/>
24+
<constraint firstAttribute="height" constant="174" id="y4R-Bz-WCE"/>
2525
</constraints>
2626
</imageView>
2727
</subviews>
2828
<viewLayoutGuide key="safeArea" id="ESo-tq-1hR"/>
2929
<color key="backgroundColor" name="launchScreen-background"/>
3030
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
3131
<constraints>
32-
<constraint firstItem="mQh-tn-BC1" firstAttribute="bottom" relation="lessThanOrEqual" secondItem="ESo-tq-1hR" secondAttribute="bottom" constant="-50" id="1xo-Se-R8K"/>
33-
<constraint firstItem="mQh-tn-BC1" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="An8-KP-MmW"/>
34-
<constraint firstItem="mQh-tn-BC1" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" priority="750" constant="-16" id="GT7-r9-bxy"/>
35-
<constraint firstItem="mQh-tn-BC1" firstAttribute="trailing" relation="lessThanOrEqual" secondItem="ESo-tq-1hR" secondAttribute="trailing" constant="50" id="h1o-xs-8hV"/>
36-
<constraint firstItem="mQh-tn-BC1" firstAttribute="top" relation="greaterThanOrEqual" secondItem="ESo-tq-1hR" secondAttribute="top" constant="50" id="nHC-N2-Sxi"/>
37-
<constraint firstItem="mQh-tn-BC1" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="ESo-tq-1hR" secondAttribute="leading" constant="50" id="vSV-am-dbH"/>
32+
<constraint firstItem="mQh-tn-BC1" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="gXx-Ta-dWp"/>
33+
<constraint firstItem="mQh-tn-BC1" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="hcu-i7-wKs"/>
3834
</constraints>
3935
</view>
4036
</viewController>

Sources/App/Resources/en.lproj/Localizable.strings

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,8 @@ Tags will work on any device with Home Assistant installed which has hardware su
444444
"off_label" = "Off";
445445
"ok_label" = "OK";
446446
"on_label" = "On";
447+
"onboarding.welcome.continue_button" = "Connect to my Home Assistant";
448+
"onboarding.welcome.secondary_button" = "Getting started";
447449
"onboarding.connect.mac_safari_warning.message" = "Try restarting Safari if the login form does not open.";
448450
"onboarding.connect.mac_safari_warning.title" = "Launching Safari";
449451
"onboarding.connect.title" = "Connecting to %@";
@@ -464,6 +466,7 @@ Tags will work on any device with Home Assistant installed which has hardware su
464466
"onboarding.manual_setup.couldnt_make_url.message" = "The value '%@' was not a valid URL.";
465467
"onboarding.manual_setup.couldnt_make_url.title" = "Could not create a URL";
466468
"onboarding.manual_setup.description" = "The URL of your Home Assistant server. Make sure it includes the protocol and port.";
469+
"onboarding.invitation.other_options" = "Other options";
467470
"onboarding.manual_setup.helper_section.title" = "Did you mean...";
468471
"onboarding.manual_setup.input_error.message" = "Make sure you have entered a valid URL.";
469472
"onboarding.manual_setup.input_error.title" = "Invalid URL";
@@ -1244,4 +1247,4 @@ Home Assistant is free and open source home automation software with a focus on
12441247
"widgets.sensors.description" = "Display state of sensors";
12451248
"widgets.sensors.not_configured" = "No Sensors Configured";
12461249
"widgets.sensors.title" = "Sensors";
1247-
"yes_label" = "Yes";
1250+
"yes_label" = "Yes";
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import Foundation
2+
import SwiftUI
3+
import UIKit
4+
5+
struct SafeAreaInsetsKey: EnvironmentKey {
6+
static var defaultValue: EdgeInsets {
7+
(
8+
UIApplication.shared.connectedScenes
9+
.compactMap { $0 as? UIWindowScene }
10+
.flatMap(\.windows)
11+
.first(where: { $0.isKeyWindow })?.safeAreaInsets ?? .zero
12+
).insets
13+
}
14+
}
15+
16+
extension EnvironmentValues {
17+
var safeAreaInsets: EdgeInsets {
18+
self[SafeAreaInsetsKey.self]
19+
}
20+
}
21+
22+
extension UIEdgeInsets {
23+
var insets: EdgeInsets {
24+
EdgeInsets(top: top, leading: left, bottom: bottom, trailing: right)
25+
}
26+
}

Sources/App/WebView/Views/WebViewEmptyStateView.swift

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -100,21 +100,3 @@ final class WebViewEmptyStateWrapperView: UIView {
100100
backgroundColor = .clear
101101
}
102102
}
103-
104-
private struct SafeAreaInsetsKey: EnvironmentKey {
105-
static var defaultValue: EdgeInsets {
106-
(UIApplication.shared.windows.first(where: { $0.isKeyWindow })?.safeAreaInsets ?? .zero).insets
107-
}
108-
}
109-
110-
private extension EnvironmentValues {
111-
var safeAreaInsets: EdgeInsets {
112-
self[SafeAreaInsetsKey.self]
113-
}
114-
}
115-
116-
private extension UIEdgeInsets {
117-
var insets: EdgeInsets {
118-
EdgeInsets(top: top, leading: left, bottom: bottom, trailing: right)
119-
}
120-
}

Sources/Shared/DesignSystem/Components/HAProgressView.swift

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import SwiftUI
22

3+
final class HAProgressViewModel: ObservableObject {
4+
@Published var isAnimating = false
5+
@Published var trimEnd: CGFloat = 0.1
6+
}
7+
38
public struct HAProgressView: View {
49
public enum Style {
510
case small
@@ -25,9 +30,7 @@ public struct HAProgressView: View {
2530
}
2631
}
2732

28-
@State private var isAnimating = false
29-
@State private var trimEnd: CGFloat = 0.0
30-
33+
@StateObject private var viewModel = HAProgressViewModel()
3134
let style: Style
3235

3336
public init(style: Style = .medium) {
@@ -40,18 +43,21 @@ public struct HAProgressView: View {
4043
.stroke(Color.track, style: StrokeStyle(lineWidth: style.lineWidth))
4144
.frame(width: style.size.width, height: style.size.height)
4245
Circle()
43-
.trim(from: 0.0, to: trimEnd)
46+
.trim(from: 0, to: viewModel.trimEnd)
4447
.stroke(
4548
Color.haPrimary,
4649
style: StrokeStyle(lineWidth: style.lineWidth, lineCap: .round)
4750
)
4851
.frame(width: style.size.width, height: style.size.height)
49-
.rotationEffect(Angle(degrees: isAnimating ? 360 : 0))
50-
.animation(Animation.linear(duration: 1).repeatForever(autoreverses: false), value: isAnimating)
52+
.rotationEffect(Angle(degrees: viewModel.isAnimating ? 360 : 0))
53+
.animation(
54+
Animation.linear(duration: 1).repeatForever(autoreverses: false),
55+
value: viewModel.isAnimating
56+
)
5157
.onAppear {
52-
isAnimating = true
58+
viewModel.isAnimating = true
5359
withAnimation(.easeInOut(duration: 1.75).repeatForever(autoreverses: true)) {
54-
trimEnd = 0.7
60+
viewModel.trimEnd = 0.7
5561
}
5662
}
5763
}

0 commit comments

Comments
 (0)