@@ -42,9 +42,11 @@ private extension DomainSelectorHostingController {
4242
4343 /// Shows a transparent navigation bar without a bottom border.
4444 func configureNavigationBarAppearance( ) {
45+ navigationItem. title = Localization . title
46+
4547 let appearance = UINavigationBarAppearance ( )
4648 appearance. configureWithTransparentBackground ( )
47- appearance. backgroundColor = UIColor . clear
49+ appearance. backgroundColor = . systemBackground
4850
4951 navigationItem. standardAppearance = appearance
5052 navigationItem. scrollEdgeAppearance = appearance
@@ -60,6 +62,7 @@ private extension DomainSelectorHostingController {
6062
6163private extension DomainSelectorHostingController {
6264 enum Localization {
65+ static let title = NSLocalizedString ( " Choose a domain " , comment: " Title of the domain selector. " )
6366 static let skipButtonTitle = NSLocalizedString ( " Skip " , comment: " Navigation bar button on the domain selector screen to skip domain selection. " )
6467 }
6568}
@@ -70,49 +73,97 @@ struct DomainSelectorView: View {
7073 var onDomainSelection : ( ( String ) -> Void ) = { _ in }
7174
7275 /// View model to drive the view.
73- @ObservedObject var viewModel : DomainSelectorViewModel
76+ @ObservedObject private var viewModel : DomainSelectorViewModel
7477
7578 /// Currently selected domain name.
7679 /// If this property is kept in the view model, a SwiftUI error appears `Publishing changes from within view updates`
7780 /// when a domain row is selected.
78- @State var selectedDomainName : String ?
81+ @State private var selectedDomainName : String ?
82+
83+ init ( viewModel: DomainSelectorViewModel ) {
84+ self . viewModel = viewModel
85+ }
7986
8087 var body : some View {
81- ScrollableVStack ( alignment: . leading) {
82- // Header labels.
83- VStack ( alignment: . leading, spacing: Layout . spacingBetweenTitleAndSubtitle) {
84- Text ( Localization . title)
85- . titleStyle ( )
86- Text ( Localization . subtitle)
87- . foregroundColor ( Color ( . secondaryLabel) )
88- . bodyStyle ( )
89- }
90- . padding ( . horizontal, Layout . defaultHorizontalPadding)
91-
92- SearchHeader ( filterText: $viewModel. searchTerm,
93- filterPlaceholder: Localization . searchPlaceholder)
94- . padding ( . horizontal, Layout . defaultHorizontalPadding)
95-
96- Text ( Localization . suggestionsHeader)
97- . foregroundColor ( Color ( . secondaryLabel) )
98- . bodyStyle ( )
99- . padding ( . horizontal, Layout . defaultHorizontalPadding)
100-
101- List ( viewModel. domains, id: \. self) { domain in
102- Button {
103- selectedDomainName = domain
104- } label: {
105- DomainRowView ( viewModel: . init( domainName: domain,
106- searchQuery: viewModel. searchTerm,
107- isSelected: domain == selectedDomainName) )
88+ VStack ( alignment: . leading, spacing: 0 ) {
89+ ScrollView {
90+ VStack ( alignment: . leading) {
91+ // Header label.
92+ Text ( Localization . subtitle)
93+ . foregroundColor ( Color ( . secondaryLabel) )
94+ . bodyStyle ( )
95+ . padding ( . horizontal, Layout . defaultHorizontalPadding)
96+
97+ // Search text field.
98+ SearchHeader ( text: $viewModel. searchTerm,
99+ placeholder: Localization . searchPlaceholder,
100+ customizations: . init( backgroundColor: . clear,
101+ borderColor: . separator,
102+ internalHorizontalPadding: 21 ,
103+ internalVerticalPadding: 12 ) )
104+
105+ // Results header.
106+ Text ( Localization . suggestionsHeader)
107+ . foregroundColor ( Color ( . secondaryLabel) )
108+ . bodyStyle ( )
109+ . padding ( . horizontal, Layout . defaultHorizontalPadding)
110+
111+ if viewModel. searchTerm. isEmpty {
112+ // Placeholder image when search query is empty.
113+ HStack {
114+ Spacer ( )
115+ Image ( uiImage: . domainSearchPlaceholderImage)
116+ Spacer ( )
117+ }
118+ } else if viewModel. isLoadingDomainSuggestions {
119+ // Progress indicator when loading domain suggestions.
120+ HStack {
121+ Spacer ( )
122+ ProgressView ( )
123+ Spacer ( )
124+ }
125+ } else if let errorMessage = viewModel. errorMessage {
126+ // Error message when there is an error loading domain suggestions.
127+ Text ( errorMessage)
128+ . padding ( Layout . defaultPadding)
129+ } else {
130+ // Domain suggestions.
131+ LazyVStack {
132+ ForEach ( viewModel. domains, id: \. self) { domain in
133+ Button {
134+ selectedDomainName = domain
135+ } label: {
136+ VStack ( alignment: . leading) {
137+ DomainRowView ( viewModel: . init( domainName: domain,
138+ searchQuery: viewModel. searchTerm,
139+ isSelected: domain == selectedDomainName) )
140+ Divider ( )
141+ . frame ( height: Layout . dividerHeight)
142+ . padding ( . leading, Layout . defaultHorizontalPadding)
143+ }
144+ }
145+ }
146+ }
147+ }
108148 }
109- } . listStyle ( . inset )
149+ }
110150
151+ // Continue button when a domain is selected.
111152 if let selectedDomainName {
153+ Divider ( )
154+ . frame ( height: Layout . dividerHeight)
155+ . foregroundColor ( Color ( . separator) )
112156 Button ( Localization . continueButtonTitle) {
113157 onDomainSelection ( selectedDomainName)
114158 }
115159 . buttonStyle ( PrimaryButtonStyle ( ) )
160+ . padding ( Layout . defaultPadding)
161+ }
162+ }
163+ . onChange ( of: viewModel. isLoadingDomainSuggestions) { isLoadingDomainSuggestions in
164+ // Resets selected domain when loading domain suggestions.
165+ if isLoadingDomainSuggestions {
166+ selectedDomainName = nil
116167 }
117168 }
118169 }
@@ -122,10 +173,11 @@ private extension DomainSelectorView {
122173 enum Layout {
123174 static let spacingBetweenTitleAndSubtitle : CGFloat = 16
124175 static let defaultHorizontalPadding : CGFloat = 16
176+ static let dividerHeight : CGFloat = 1
177+ static let defaultPadding : EdgeInsets = . init( top: 10 , leading: 16 , bottom: 10 , trailing: 16 )
125178 }
126179
127180 enum Localization {
128- static let title = NSLocalizedString ( " Choose a domain " , comment: " Title of the domain selector. " )
129181 static let subtitle = NSLocalizedString (
130182 " This is where people will find you on the Internet. Don't worry, you can change it later. " ,
131183 comment: " Subtitle of the domain selector. " )
@@ -135,8 +187,62 @@ private extension DomainSelectorView {
135187 }
136188}
137189
190+ #if DEBUG
191+
192+ import Yosemite
193+ import enum Networking. DotcomError
194+
195+ /// StoresManager that specifically handles `DomainAction` for `DomainSelectorView` previews.
196+ final class DomainSelectorViewStores : DefaultStoresManager {
197+ private let result : Result < [ FreeDomainSuggestion ] , Error > ?
198+
199+ init ( result: Result < [ FreeDomainSuggestion ] , Error > ? ) {
200+ self . result = result
201+ super. init ( sessionManager: ServiceLocator . stores. sessionManager)
202+ }
203+
204+ override func dispatch( _ action: Action ) {
205+ if let action = action as? DomainAction {
206+ if case let . loadFreeDomainSuggestions( _, completion) = action {
207+ if let result {
208+ completion ( result)
209+ }
210+ }
211+ }
212+ }
213+ }
214+
138215struct DomainSelectorView_Previews : PreviewProvider {
139216 static var previews : some View {
140- DomainSelectorView ( viewModel: . init( ) )
217+ Group {
218+ // Empty query state.
219+ DomainSelectorView ( viewModel:
220+ . init( initialSearchTerm: " " ,
221+ stores: DomainSelectorViewStores ( result: nil ) ) )
222+ // Results state.
223+ DomainSelectorView ( viewModel:
224+ . init( initialSearchTerm: " Fruit smoothie " ,
225+ stores: DomainSelectorViewStores ( result: . success( [
226+ . init( name: " grapefruitsmoothie.com " , isFree: true ) ,
227+ . init( name: " fruitsmoothie.com " , isFree: true ) ,
228+ . init( name: " grapefruitsmoothiee.com " , isFree: true ) ,
229+ . init( name: " freesmoothieeee.com " , isFree: true ) ,
230+ . init( name: " greatfruitsmoothie1.com " , isFree: true ) ,
231+ . init( name: " tropicalsmoothie.com " , isFree: true )
232+ ] ) ) ) )
233+ // Error state.
234+ DomainSelectorView ( viewModel:
235+ . init( initialSearchTerm: " test " ,
236+ stores: DomainSelectorViewStores ( result: . failure(
237+ DotcomError . unknown ( code: " invalid_query " ,
238+ message: " Domain searches must contain a word with the following characters. " )
239+ ) ) ) )
240+ // Loading state.
241+ DomainSelectorView ( viewModel:
242+ . init( initialSearchTerm: " test " ,
243+ stores: DomainSelectorViewStores ( result: nil ) ) )
244+ }
141245 }
142246}
247+
248+ #endif
0 commit comments