@@ -4,18 +4,14 @@ import SwiftUI
44final class DomainSelectorHostingController : UIHostingController < DomainSelectorView > {
55 private let viewModel : DomainSelectorViewModel
66 private let onDomainSelection : ( String ) async -> Void
7- private let onSkip : ( ) -> Void
87
98 /// - Parameters:
109 /// - viewModel: View model for the domain selector.
1110 /// - onDomainSelection: Called when the user continues with a selected domain name.
12- /// - onSkip: Called when the user taps to skip domain selection.
1311 init ( viewModel: DomainSelectorViewModel ,
14- onDomainSelection: @escaping ( String ) async -> Void ,
15- onSkip: @escaping ( ) -> Void ) {
12+ onDomainSelection: @escaping ( String ) async -> Void ) {
1613 self . viewModel = viewModel
1714 self . onDomainSelection = onDomainSelection
18- self . onSkip = onSkip
1915 super. init ( rootView: DomainSelectorView ( viewModel: viewModel) )
2016
2117 rootView. onDomainSelection = { [ weak self] domain in
@@ -30,16 +26,11 @@ final class DomainSelectorHostingController: UIHostingController<DomainSelectorV
3026 override func viewDidLoad( ) {
3127 super. viewDidLoad ( )
3228
33- configureSkipButton ( )
3429 configureNavigationBarAppearance ( )
3530 }
3631}
3732
3833private extension DomainSelectorHostingController {
39- func configureSkipButton( ) {
40- navigationItem. rightBarButtonItem = . init( title: Localization . skipButtonTitle, style: . plain, target: self , action: #selector( skipButtonTapped) )
41- }
42-
4334 /// Shows a transparent navigation bar without a bottom border.
4435 func configureNavigationBarAppearance( ) {
4536 let appearance = UINavigationBarAppearance ( )
@@ -52,20 +43,20 @@ private extension DomainSelectorHostingController {
5243 }
5344}
5445
55- private extension DomainSelectorHostingController {
56- @objc func skipButtonTapped( ) {
57- onSkip ( )
58- }
59- }
60-
61- private extension DomainSelectorHostingController {
62- enum Localization {
63- static let skipButtonTitle = NSLocalizedString ( " Skip " , comment: " Navigation bar button on the domain selector screen to skip domain selection. " )
64- }
65- }
66-
6746/// Allows the user to search for a domain and then select one to continue.
6847struct DomainSelectorView : View {
48+ /// The state of the main view below the fixed header.
49+ enum ViewState : Equatable {
50+ /// When loading domain suggestions.
51+ case loading
52+ /// Shown when the search query is empty.
53+ case placeholder
54+ /// When there is an error loading domain suggestions.
55+ case error( message: String )
56+ /// When domain suggestions are displayed.
57+ case results( domains: [ String ] )
58+ }
59+
6960 /// Set in the hosting controller.
7061 var onDomainSelection : ( ( String ) async -> Void ) = { _ in }
7162
@@ -79,88 +70,113 @@ struct DomainSelectorView: View {
7970
8071 @State private var isWaitingForDomainSelectionCompletion : Bool = false
8172
73+ @FocusState private var textFieldIsFocused : Bool
74+
8275 init ( viewModel: DomainSelectorViewModel ) {
8376 self . viewModel = viewModel
8477 }
8578
8679 var body : some View {
87- VStack ( alignment : . leading , spacing : 0 ) {
88- ScrollView {
89- VStack ( alignment : . leading ) {
90- // Header label.
91- Text ( Localization . subtitle )
92- . foregroundColor ( Color ( . secondaryLabel ) )
93- . bodyStyle ( )
94- . padding ( . horizontal , Layout . defaultHorizontalPadding )
80+ ScrollView {
81+ VStack ( alignment : . leading , spacing : 0 ) {
82+ // Header label.
83+ Text ( Localization . subtitle )
84+ . foregroundColor ( Color ( . secondaryLabel ) )
85+ . bodyStyle ( )
86+ . padding ( . horizontal , Layout . defaultHorizontalPadding )
87+ . padding ( . top , 16 )
9588
96- // Search text field.
97- SearchHeader ( text: $viewModel. searchTerm,
98- placeholder: Localization . searchPlaceholder,
99- customizations: . init( backgroundColor: . clear,
100- borderColor: . separator,
101- internalHorizontalPadding: 21 ,
102- internalVerticalPadding: 12 ) )
89+ Spacer ( )
90+ . frame ( height: 30 )
10391
104- // Results header.
105- Text ( Localization . suggestionsHeader)
92+ // Search text field.
93+ SearchHeader ( text: $viewModel. searchTerm,
94+ placeholder: Localization . searchPlaceholder,
95+ customizations: . init( backgroundColor: . clear,
96+ borderColor: . separator,
97+ internalHorizontalPadding: 21 ,
98+ internalVerticalPadding: 12 ,
99+ iconSize: . init( width: 14 , height: 14 ) ) )
100+ . focused ( $textFieldIsFocused)
101+
102+ switch viewModel. state {
103+ case . placeholder:
104+ // Placeholder image when search query is empty.
105+ Spacer ( )
106+ . frame ( height: 30 )
107+
108+ HStack {
109+ Spacer ( )
110+ Image ( uiImage: . domainSearchPlaceholderImage)
111+ Spacer ( )
112+ }
113+ case . loading:
114+ // Progress indicator when loading domain suggestions.
115+ Spacer ( )
116+ . frame ( height: 23 )
117+
118+ HStack {
119+ Spacer ( )
120+ ProgressView ( )
121+ Spacer ( )
122+ }
123+ case . error( let errorMessage) :
124+ // Error message when there is an error loading domain suggestions.
125+ Spacer ( )
126+ . frame ( height: 23 )
127+
128+ Text ( errorMessage)
106129 . foregroundColor ( Color ( . secondaryLabel) )
107130 . bodyStyle ( )
131+ . frame ( maxWidth: . infinity, alignment: . center)
132+ . multilineTextAlignment ( . center)
133+ . padding ( Layout . defaultPadding)
134+ case . results( let domains) :
135+ // Domain suggestions.
136+ Text ( Localization . suggestionsHeader)
137+ . foregroundColor ( Color ( . secondaryLabel) )
138+ . footnoteStyle ( )
108139 . padding ( . horizontal, Layout . defaultHorizontalPadding)
140+ . padding ( . vertical, insets: . init( top: 14 , leading: 0 , bottom: 8 , trailing: 0 ) )
109141
110- if viewModel. searchTerm. isEmpty {
111- // Placeholder image when search query is empty.
112- HStack {
113- Spacer ( )
114- Image ( uiImage: . domainSearchPlaceholderImage)
115- Spacer ( )
116- }
117- } else if viewModel. isLoadingDomainSuggestions {
118- // Progress indicator when loading domain suggestions.
119- HStack {
120- Spacer ( )
121- ProgressView ( )
122- Spacer ( )
123- }
124- } else if let errorMessage = viewModel. errorMessage {
125- // Error message when there is an error loading domain suggestions.
126- Text ( errorMessage)
127- . padding ( Layout . defaultPadding)
128- } else {
129- // Domain suggestions.
130- LazyVStack {
131- ForEach ( viewModel. domains, id: \. self) { domain in
132- Button {
133- selectedDomainName = domain
134- } label: {
135- VStack ( alignment: . leading) {
136- DomainRowView ( viewModel: . init( domainName: domain,
137- searchQuery: viewModel. searchTerm,
138- isSelected: domain == selectedDomainName) )
139- Divider ( )
140- . frame ( height: Layout . dividerHeight)
141- . padding ( . leading, Layout . defaultHorizontalPadding)
142- }
142+ LazyVStack {
143+ ForEach ( domains, id: \. self) { domain in
144+ Button {
145+ textFieldIsFocused = false
146+ selectedDomainName = domain
147+ } label: {
148+ VStack ( alignment: . leading) {
149+ DomainRowView ( viewModel: . init( domainName: domain,
150+ searchQuery: viewModel. searchTerm,
151+ isSelected: domain == selectedDomainName) )
152+ Divider ( )
153+ . frame ( height: Layout . dividerHeight)
154+ . padding ( . leading, Layout . defaultHorizontalPadding)
143155 }
144156 }
145157 }
146158 }
147159 }
148160 }
149-
161+ }
162+ . safeAreaInset ( edge: . bottom) {
150163 // Continue button when a domain is selected.
151164 if let selectedDomainName {
152- Divider ( )
153- . frame ( height: Layout . dividerHeight)
154- . foregroundColor ( Color ( . separator) )
155- Button ( Localization . continueButtonTitle) {
156- Task { @MainActor in
157- isWaitingForDomainSelectionCompletion = true
158- await onDomainSelection ( selectedDomainName)
159- isWaitingForDomainSelectionCompletion = false
165+ VStack {
166+ Divider ( )
167+ . frame ( height: Layout . dividerHeight)
168+ . foregroundColor ( Color ( . separator) )
169+ Button ( Localization . continueButtonTitle) {
170+ Task { @MainActor in
171+ isWaitingForDomainSelectionCompletion = true
172+ await onDomainSelection ( selectedDomainName)
173+ isWaitingForDomainSelectionCompletion = false
174+ }
160175 }
176+ . buttonStyle ( PrimaryLoadingButtonStyle ( isLoading: isWaitingForDomainSelectionCompletion) )
177+ . padding ( Layout . defaultPadding)
161178 }
162- . buttonStyle ( PrimaryLoadingButtonStyle ( isLoading: isWaitingForDomainSelectionCompletion) )
163- . padding ( Layout . defaultPadding)
179+ . background ( Color ( . systemBackground) )
164180 }
165181 }
166182 . navigationTitle ( Localization . title)
@@ -176,7 +192,6 @@ struct DomainSelectorView: View {
176192
177193private extension DomainSelectorView {
178194 enum Layout {
179- static let spacingBetweenTitleAndSubtitle : CGFloat = 16
180195 static let defaultHorizontalPadding : CGFloat = 16
181196 static let dividerHeight : CGFloat = 1
182197 static let defaultPadding : EdgeInsets = . init( top: 10 , leading: 16 , bottom: 10 , trailing: 16 )
@@ -185,9 +200,9 @@ private extension DomainSelectorView {
185200 enum Localization {
186201 static let title = NSLocalizedString ( " Choose a domain " , comment: " Title of the domain selector. " )
187202 static let subtitle = NSLocalizedString (
188- " This is where people will find you on the Internet. Don't worry, you can change it later. " ,
203+ " This is where people will find you on the Internet. You can add another domain later. " ,
189204 comment: " Subtitle of the domain selector. " )
190- static let searchPlaceholder = NSLocalizedString ( " Type to get suggestions " , comment: " Placeholder of the search text field on the domain selector. " )
205+ static let searchPlaceholder = NSLocalizedString ( " Type a name for your store " , comment: " Placeholder of the search text field on the domain selector. " )
191206 static let suggestionsHeader = NSLocalizedString ( " SUGGESTIONS " , comment: " Header label of the domain suggestions on the domain selector. " )
192207 static let continueButtonTitle = NSLocalizedString ( " Continue " , comment: " Title of the button to continue with a selected domain. " )
193208 }
0 commit comments