Add hop list view

This commit is contained in:
Rokas Ambrazevicius
2024-01-12 16:36:16 +02:00
parent 8e302bfdde
commit 85ccd835ab
18 changed files with 297 additions and 41 deletions
+2 -1
View File
@@ -21,7 +21,8 @@ let package = Package(
targets: [
.target(
name: "Home",
dependencies: ["UIComponents"]
dependencies: ["UIComponents"],
path: "Sources"
),
.testTarget(
name: "HomeTests",
+3 -4
View File
@@ -28,8 +28,7 @@ private extension HomeView {
func navbar() -> some View {
CustomNavBar(
title: "NymVPN".localizedString,
leftButton: CustomNavBarButton(type: .empty, action: {}),
rightButton: CustomNavBarButton(type: .settingsGear, action: {})
rightButton: CustomNavBarButton(type: .settings, action: {})
)
Spacer()
.frame(height: 50)
@@ -86,10 +85,10 @@ private extension HomeView {
.frame(height: 24)
VStack {
HopButton(country: Country(name: "Germany", code: "de"))
HopButton(hopType: .first, country: Country(name: "Germany", code: "de"))
Spacer()
.frame(height: 24)
HopButton(country: Country(name: "Switzerland", code: "ch"))
HopButton(hopType: .last, country: Country(name: "Switzerland", code: "ch"))
}
.padding(.horizontal, 16)
@@ -0,0 +1,51 @@
import SwiftUI
import Theme
import UIComponents
public struct HopListView: View {
private let viewModel: HopListViewModel
public init(viewModel: HopListViewModel) {
self.viewModel = viewModel
}
public var body: some View {
VStack {
navbar()
Spacer()
.frame(height: 24)
searchView()
Spacer()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background {
NymColor.background
.ignoresSafeArea()
}
}
}
private extension HopListView {
@ViewBuilder
func navbar() -> some View {
CustomNavBar(
title: viewModel.type.selectHopLocalizedTitle,
leftButton: CustomNavBarButton(type: .back, action: {})
)
}
@ViewBuilder
func searchView() -> some View {
SearchView(viewModel: SearchViewModel())
.padding(.horizontal, 16)
}
}
public struct HopListViewModel {
public let type: HopType
public init(type: HopType) {
self.type = type
}
}
@@ -0,0 +1,15 @@
import Foundation
public enum HopType {
case first
case last
var selectHopLocalizedTitle: String {
switch self {
case .first:
"firstHopSelection".localizedString
case .last:
"lastHopSelection".localizedString
}
}
}
@@ -5,9 +5,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "0.080",
"blue" : "0.443",
"green" : "0.357",
"red" : "0.384"
"blue" : "0x71",
"green" : "0x5B",
"red" : "0x61"
}
},
"idiom" : "universal"
@@ -23,9 +23,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "0.160",
"blue" : "0.200",
"green" : "0.188",
"red" : "0.192"
"blue" : "0x33",
"green" : "0x30",
"red" : "0x30"
}
},
"idiom" : "universal"
@@ -5,9 +5,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "31",
"green" : "27",
"red" : "28"
"blue" : "255",
"green" : "255",
"red" : "255"
}
},
"idiom" : "universal"
@@ -23,9 +23,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "255",
"green" : "255",
"red" : "255"
"blue" : "31",
"green" : "27",
"red" : "28"
}
},
"idiom" : "universal"
@@ -122,6 +122,39 @@
}
}
},
"firstHopSelection" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "First hop selection"
}
}
}
},
"lastHop" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Last hop"
}
}
}
},
"lastHopSelection" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Last hop selection"
}
}
}
},
"NymVPN" : {
"extractionState" : "manual",
"localizations" : {
@@ -133,6 +166,28 @@
}
}
},
"search" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Search"
}
}
}
},
"searchCountry" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Search country"
}
}
}
},
"selectNetwork" : {
"extractionState" : "manual",
"localizations" : {
@@ -6,7 +6,7 @@ public struct ConnectButton: View {
public var body: some View {
HStack {
Text("Connect")
Text("connect".localizedString)
.foregroundStyle(NymColor.connectTitle)
.textStyle(.Label.Huge.primary)
}
@@ -2,14 +2,16 @@ import SwiftUI
import Theme
public struct HopButton: View {
private let hopType: HopType
private let country: Country
public init(country: Country) {
public init(hopType: HopType, country: Country) {
self.hopType = hopType
self.country = country
}
public var body: some View {
VStack(alignment: .leading) {
StrokeBorderView(strokeTitle: hopType.localizedTitle) {
HStack {
Image(country.code)
.resizable()
@@ -27,22 +29,6 @@ public struct HopButton: View {
.padding(16)
}
}
.padding(0)
.frame(maxWidth: .infinity, minHeight: 56, maxHeight: 56)
.cornerRadius(8)
.overlay {
RoundedRectangle(cornerRadius: 8)
.inset(by: 0.5)
.stroke(Color(red: 0.29, green: 0.27, blue: 0.31), lineWidth: 1)
}
.overlay(alignment: .topLeading) {
Text("firstHop".localizedString)
.foregroundStyle(NymColor.sysOnSurface)
.textStyle(.Body.Small.primary)
.padding(4)
.background(NymColor.background)
.position(x: 40, y: 0)
}
}
}
@@ -55,3 +41,17 @@ public struct Country {
self.code = code
}
}
public enum HopType {
case first
case last
var localizedTitle: String {
switch self {
case .first:
"firstHop".localizedString
case .last:
"lastHop".localizedString
}
}
}
@@ -8,8 +8,8 @@ public struct CustomNavBar: View {
public init(
title: String,
leftButton: CustomNavBarButton? = nil,
rightButton: CustomNavBarButton? = nil
leftButton: CustomNavBarButton? = CustomNavBarButton(type: .empty, action: {}),
rightButton: CustomNavBarButton? = CustomNavBarButton(type: .empty, action: {})
) {
self.title = title
self.leftButton = leftButton
@@ -3,13 +3,16 @@ import Theme
public struct CustomNavBarButton: View {
public enum ButtonType: String {
case settingsGear
case back
case settings
case empty
var imageName: String? {
switch self {
case .settingsGear:
self.rawValue
case .back:
"arrowBack"
case .settings:
"settingsGear"
case .empty:
nil
}
@@ -0,0 +1,16 @@
{
"images" : [
{
"filename" : "arrow_back_24px.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Icons/arrow_back_24px">
<path id="icon" d="M20 11H7.83L13.42 5.41L12 4L4 12L12 20L13.41 18.59L7.83 13H20V11Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 240 B

@@ -0,0 +1,16 @@
{
"images" : [
{
"filename" : "search_24px.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Icons/search_24px">
<path id="icon" fill-rule="evenodd" clip-rule="evenodd" d="M14.76 13.27L20.49 19L19 20.49L13.27 14.76C12.2 15.53 10.91 16 9.5 16C5.91 16 3 13.09 3 9.5C3 5.91 5.91 3 9.5 3C13.09 3 16 5.91 16 9.5C16 10.91 15.53 12.2 14.76 13.27ZM9.5 5C7.01 5 5 7.01 5 9.5C5 11.99 7.01 14 9.5 14C11.99 14 14 11.99 14 9.5C14 7.01 11.99 5 9.5 5Z" fill="#CAC4D0"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 477 B

@@ -0,0 +1,45 @@
import SwiftUI
import Theme
public struct SearchView: View {
@ObservedObject private var viewModel: SearchViewModel
public init(viewModel: SearchViewModel) {
self.viewModel = viewModel
}
public var body: some View {
StrokeBorderView(strokeTitle: viewModel.strokeTitle) {
HStack {
searchImage()
searchTextfield()
Spacer()
}
}
}
}
extension SearchView {
@ViewBuilder
func searchImage() -> some View {
Image(viewModel.searchImageName, bundle: .module)
.resizable()
.frame(width: 24, height: 24)
.cornerRadius(50)
.padding(16)
}
@ViewBuilder
func searchTextfield() -> some View {
ZStack(alignment: .leading) {
if viewModel.searchText.isEmpty {
Text(viewModel.searchCountryTitle)
.foregroundStyle(NymColor.sysOutline)
.textStyle(.Body.Large.primary)
}
TextField("", text: $viewModel.searchText)
.foregroundStyle(NymColor.sysOnSurface)
.textStyle(.Body.Large.primary)
}
}
}
@@ -0,0 +1,11 @@
import Foundation
public final class SearchViewModel: ObservableObject {
let strokeTitle = "search".localizedString
let searchCountryTitle = "searchCountry".localizedString
let searchImageName = "searchIcon"
@Published var searchText = ""
public init() {}
}
@@ -0,0 +1,34 @@
import SwiftUI
import Theme
public struct StrokeBorderView<Content: View>: View {
@ViewBuilder private let content: Content
private let strokeTitle: String
public init(strokeTitle: String, @ViewBuilder content: () -> Content) {
self.strokeTitle = strokeTitle
self.content = content()
}
public var body: some View {
VStack(alignment: .leading) {
content
}
.padding(0)
.frame(maxWidth: .infinity, minHeight: 56, maxHeight: 56)
.cornerRadius(8)
.overlay {
RoundedRectangle(cornerRadius: 8)
.inset(by: 0.5)
.stroke(Color(red: 0.29, green: 0.27, blue: 0.31), lineWidth: 1)
}
.overlay(alignment: .topLeading) {
Text(strokeTitle)
.foregroundStyle(NymColor.sysOnSurface)
.textStyle(.Body.Small.primary)
.padding(4)
.background(NymColor.background)
.position(x: 40, y: 0)
}
}
}