From 19fec3cbbdb81df5bd72892c074f9cfd80954024 Mon Sep 17 00:00:00 2001 From: Himanshu Neema Date: Fri, 20 Sep 2024 12:35:52 -0700 Subject: [PATCH] iOS 1.0.0 (5) source (#29) --- .../UpVPN/App/Views/LocationsMapView.swift | 144 ++++++++++++ .../UpVPN/App/Views/LocationsView.swift | 18 ++ .../App/Views/RuntimeConfigurationView.swift | 17 +- upvpn-apple/UpVPN/Config/Version.xcconfig | 2 +- .../UpVPN/UpVPN.xcodeproj/project.pbxproj | 12 + .../UpVPN/iOS/Views/MainViewContent.swift | 28 ++- .../UpVPN/iOS/Views/ResponsiveHomeView.swift | 209 ++++++++++++++++++ .../iOS/Views/RuntimeConfigMapView.swift | 55 +++++ 8 files changed, 480 insertions(+), 5 deletions(-) create mode 100644 upvpn-apple/UpVPN/App/Views/LocationsMapView.swift create mode 100644 upvpn-apple/UpVPN/iOS/Views/ResponsiveHomeView.swift create mode 100644 upvpn-apple/UpVPN/iOS/Views/RuntimeConfigMapView.swift diff --git a/upvpn-apple/UpVPN/App/Views/LocationsMapView.swift b/upvpn-apple/UpVPN/App/Views/LocationsMapView.swift new file mode 100644 index 0000000..1b97881 --- /dev/null +++ b/upvpn-apple/UpVPN/App/Views/LocationsMapView.swift @@ -0,0 +1,144 @@ +// +// LocationsMapView.swift +// UpVPNiOS +// +// Created by Himanshu on 9/3/24. +// + +import SwiftUI +import MapKit + +func getLatLong(_ location: Location) -> (CLLocationDegrees, CLLocationDegrees)? { + switch location.code { + case "au_vic_melbourne": return (-37.813629, 144.963058) + case "au_nsw_sydney": return (-33.872710,151.205694) + case "br_sao_paulo": return (-23.579640,-46.655065) + case "ca_qc_montreal": return (45.507773,-73.554461) + case "ca_on_toronto": return (43.651605,-79.383125) + case "cl_santiago": return (-33.451856,-70.650466) + case "gb_london": return (51.503347,-0.079396) + case "gb_manchester": return (53.479606,-2.245505) + case "fi_helsinki": return (60.167928, 24.952984) + case "fr_paris": return (48.856788, 2.351077) + case "de_by_falkenstein": return (50.477179, 12.365762) + case "de_he_frankfurt": return (50.110556, 8.680173) + case "de_by_nuremberg": return (49.454473, 11.076937) + case "in_ka_bengaluru": return (12.977405, 77.574234) + case "in_tn_chennai": return (13.082881, 80.276002) + case "in_delhi": return (28.626963, 77.215396) + case "in_mh_mumbai": return (18.940100, 72.834659) + case "id_jakarta": return (-6.212922, 106.848723) + case "ireland": return (53.422433, -7.929837) + case "il_tel_aviv": return (32.084454, 34.785621) + case "it_milan": return (45.467175, 9.189664) + case "jp_osaka": return (34.693726, 135.502162) + case "jp_tokyo": return (35.689506, 139.691700) + case "mx_mexico_city": return (19.430105, -99.133607) + case "nl_amsterdam": return (52.401404, 4.931969) + case "pl_warsaw": return (52.243427, 21.001797) + case "singapore": return (1.304374, 103.824580) + case "sa_johannesburg": return (-26.202270, 28.043630) + case "kr_seoul": return (37.566983, 126.978235) + case "es_madrid": return (40.419213, -3.692517) + case "se_stockholm": return (59.327875, 18.053265) + case "us_va_ashburn": return (39.046636, -77.471291) + case "us_ga_atlanta": return (33.748188, -84.390865) + case "us_il_chicago": return (41.883718, -87.632382) + case "us_tx_dallas": return (32.775568, -96.795595) + case "us_ca_fremont": return (37.552329, -121.983005) + case "us_or_hillsboro": return (45.522667, -122.989020) + case "us_hi_honolulu": return (21.304687, -157.857388) + case "us_ca_los_angeles": return (34.053345, -118.242349) + case "us_fl_miami": return (25.770843, -80.197636) + case "us_nj_newark": return (40.732026, -74.174184) + case "us_ny_newyork": return (40.712982, -74.007205) + case "us_ohio": return (39.962522, -82.997972) + case "us_oregon": return (44.941253, -123.029020) + case "us_ca_san_francisco": return (37.779379, -122.418433) + case "us_wa_seattle": return (47.603776, -122.330765) + case "us_ca_silicon_valley": return (37.337213, -121.887090) + case "us_virginia": return (37.540829, -77.433881) + case "us_washington_dc": return (38.895438, -77.031281) + default: + return nil + } +} + +func getCoordinate(_ location: Location) -> CLLocationCoordinate2D? { + getLatLong(location).map { (lat, long) in CLLocationCoordinate2DMake(lat, long) } +} + +@available(iOS 17, *) +enum CoordinateSpan { + case small + case large + + func toMKCoordinateSpan() -> MKCoordinateSpan { + switch self { + case .small: + LocationsMapView.coordinateSpanSmall + case .large: + LocationsMapView.coordinateSpanLarge + } + } +} + +@available(iOS 17, *) +struct LocationsMapView: View { + + @EnvironmentObject private var locationViewModel: LocationViewModel + + @State private var mapCameraPosition: MapCameraPosition = .automatic + + static var coordinateSpanSmall: MKCoordinateSpan = MKCoordinateSpan(latitudeDelta: 10, longitudeDelta: 10) + static var coordinateSpanLarge: MKCoordinateSpan = MKCoordinateSpan(latitudeDelta: 50, longitudeDelta: 50) + + var coordinateSpan: CoordinateSpan = CoordinateSpan.small + + var body: some View { + Map(position: $mapCameraPosition, selection: locationViewModel.selectionBinding) { + ForEach(locationViewModel.locations) { location in + if location == locationViewModel.selected { + if let coordinate = getCoordinate(location) { + Annotation("", coordinate: coordinate) { + LocationView(location: location) + .background(Color.uSecondarySystemGroupedBackground) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .environmentObject(locationViewModel) + } + .tag(location) + } + } else { + if let coordinate = getCoordinate(location) { + Marker(location.city, coordinate: coordinate) + .tag(location) + } + } + } + } + .ignoresSafeArea() + .mapStyle(.standard) + .onAppear { + if let location = locationViewModel.selected { + if let coordinate = getCoordinate(location) { + mapCameraPosition = .region(MKCoordinateRegion(center: coordinate, + span: coordinateSpan.toMKCoordinateSpan())) + } + } + } + .onChange(of: locationViewModel.selected) { + if let location = locationViewModel.selected { + if let coordinate = getCoordinate(location) { + mapCameraPosition = .region(MKCoordinateRegion(center: coordinate, + span: coordinateSpan.toMKCoordinateSpan())) + } + } + } + } +} + +@available(iOS 17, *) +#Preview { + LocationsMapView() + .environmentObject(LocationViewModel(dataRepository: DataRepository.shared, isDisconnected: { return true })) +} diff --git a/upvpn-apple/UpVPN/App/Views/LocationsView.swift b/upvpn-apple/UpVPN/App/Views/LocationsView.swift index 5ad9897..dd63f55 100644 --- a/upvpn-apple/UpVPN/App/Views/LocationsView.swift +++ b/upvpn-apple/UpVPN/App/Views/LocationsView.swift @@ -13,6 +13,8 @@ struct LocationsView: View { @State private var search: String = "" + var showMapInToolbar: Bool = false + private var filteredLocations: [Location] { return if search.isEmpty { locationViewModel.locations @@ -40,6 +42,22 @@ struct LocationsView: View { .onAppear { locationViewModel.reload() } + .toolbar { + if showMapInToolbar { + #if os(iOS) + if #available(iOS 17, *) { + NavigationLink { + LocationsMapView() + .navigationTitle("Locations Map") + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.hidden, for: .automatic) + } label: { + Label("Map", systemImage: "map") + } + } + #endif + } + } } } diff --git a/upvpn-apple/UpVPN/App/Views/RuntimeConfigurationView.swift b/upvpn-apple/UpVPN/App/Views/RuntimeConfigurationView.swift index bfee596..8b637cf 100644 --- a/upvpn-apple/UpVPN/App/Views/RuntimeConfigurationView.swift +++ b/upvpn-apple/UpVPN/App/Views/RuntimeConfigurationView.swift @@ -50,11 +50,26 @@ struct RuntimeConfigurationView: View { } #if os(iOS) // only available on iOS - .listStyle(.grouped) + .modifier(ListStyleModifer()) #endif } } +#if os(iOS) +struct ListStyleModifer : ViewModifier { + + func body(content: Content) -> some View { + if UIDevice.current.userInterfaceIdiom == .pad { + content + .listStyle(.insetGrouped) + } else { + content + .listStyle(.grouped) + } + } +} +#endif + #Preview { RuntimeConfigurationView() .environmentObject(TunnelViewModel()) diff --git a/upvpn-apple/UpVPN/Config/Version.xcconfig b/upvpn-apple/UpVPN/Config/Version.xcconfig index 8209693..1412ea6 100644 --- a/upvpn-apple/UpVPN/Config/Version.xcconfig +++ b/upvpn-apple/UpVPN/Config/Version.xcconfig @@ -1,2 +1,2 @@ VERSION_NAME = 1.0.0 -VERSION_ID = 4 +VERSION_ID = 5 diff --git a/upvpn-apple/UpVPN/UpVPN.xcodeproj/project.pbxproj b/upvpn-apple/UpVPN/UpVPN.xcodeproj/project.pbxproj index 454e2f3..3ca56b4 100644 --- a/upvpn-apple/UpVPN/UpVPN.xcodeproj/project.pbxproj +++ b/upvpn-apple/UpVPN/UpVPN.xcodeproj/project.pbxproj @@ -164,6 +164,9 @@ A391D9AA2C4979080008D23F /* DataRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = A391D9A62C4979080008D23F /* DataRepository.swift */; }; A391D9AB2C49867A0008D23F /* UserStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A391D9892C4919FA0008D23F /* UserStore.swift */; }; A391D9AC2C49867B0008D23F /* UserStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A391D9892C4919FA0008D23F /* UserStore.swift */; }; + A3A21D892C87118100600C2C /* ResponsiveHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3A21D882C87118100600C2C /* ResponsiveHomeView.swift */; }; + A3A21D8B2C87362E00600C2C /* LocationsMapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3A21D8A2C87362E00600C2C /* LocationsMapView.swift */; }; + A3A21D8D2C8773E800600C2C /* RuntimeConfigMapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3A21D8C2C8773E800600C2C /* RuntimeConfigMapView.swift */; }; A3A7635C2C4EF5B00088D5F5 /* AppConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3A7635B2C4EF5B00088D5F5 /* AppConfig.swift */; }; A3A7635D2C4EF5B00088D5F5 /* AppConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3A7635B2C4EF5B00088D5F5 /* AppConfig.swift */; }; A3A7635E2C4EF5B00088D5F5 /* AppConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3A7635B2C4EF5B00088D5F5 /* AppConfig.swift */; }; @@ -357,6 +360,9 @@ A391D99B2C4954A00008D23F /* AuthViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthViewModel.swift; sourceTree = ""; }; A391D99E2C4968130008D23F /* Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Types.swift; sourceTree = ""; }; A391D9A62C4979080008D23F /* DataRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataRepository.swift; sourceTree = ""; }; + A3A21D882C87118100600C2C /* ResponsiveHomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponsiveHomeView.swift; sourceTree = ""; }; + A3A21D8A2C87362E00600C2C /* LocationsMapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationsMapView.swift; sourceTree = ""; }; + A3A21D8C2C8773E800600C2C /* RuntimeConfigMapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeConfigMapView.swift; sourceTree = ""; }; A3A7635B2C4EF5B00088D5F5 /* AppConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConfig.swift; sourceTree = ""; }; A3A763602C50122C0088D5F5 /* VPNSessionRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNSessionRepository.swift; sourceTree = ""; }; A3A763632C501B210088D5F5 /* VPNOrchestratorState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNOrchestratorState.swift; sourceTree = ""; }; @@ -591,6 +597,7 @@ A36566072C556EFE00C2E748 /* HomeView.swift */, A36566502C56E75600C2E748 /* LocationView.swift */, A365660A2C556FF500C2E748 /* LocationsView.swift */, + A3A21D8A2C87362E00600C2C /* LocationsMapView.swift */, A365660D2C55700200C2E748 /* SettingsView.swift */, A365664A2C56C66500C2E748 /* HomeCard.swift */, A365664D2C56C67500C2E748 /* RecentLocationsCard.swift */, @@ -660,6 +667,8 @@ isa = PBXGroup; children = ( A3A9F0F22C6E704900AF1490 /* MainViewContent.swift */, + A3A21D882C87118100600C2C /* ResponsiveHomeView.swift */, + A3A21D8C2C8773E800600C2C /* RuntimeConfigMapView.swift */, ); path = Views; sourceTree = ""; @@ -1071,6 +1080,7 @@ A365664F2C56C67500C2E748 /* RecentLocationsCard.swift in Sources */, A33336CF2C47A158004D8C8E /* StoreKeys.swift in Sources */, A33336CA2C479D9F004D8C8E /* Device.swift in Sources */, + A3A21D892C87118100600C2C /* ResponsiveHomeView.swift in Sources */, A36566092C556EFE00C2E748 /* HomeView.swift in Sources */, A3A9F0FC2C6FE59200AF1490 /* SignOutView.swift in Sources */, A3422DE72C3DB4DE00A3D867 /* TunnelObserver.swift in Sources */, @@ -1099,6 +1109,7 @@ A3422DCF2C3879FD00A3D867 /* Request.swift in Sources */, A391D98B2C4919FA0008D23F /* UserStore.swift in Sources */, A3FABF9D2C6B4F02004753D1 /* PlanView.swift in Sources */, + A3A21D8B2C87362E00600C2C /* LocationsMapView.swift in Sources */, A323FAE62C57FD4C00CF9D84 /* ElapsedTimeView.swift in Sources */, A347A8242C59B80500FEF961 /* StatsCard.swift in Sources */, A3A9F0F62C6E934300AF1490 /* RuntimeConfigurationView.swift in Sources */, @@ -1115,6 +1126,7 @@ A391D99D2C4954A00008D23F /* AuthViewModel.swift in Sources */, A36565F92C52E5F900C2E748 /* TunnelConfiguration+UapiConfig.swift in Sources */, A3A763742C521EF80088D5F5 /* LocationStore.swift in Sources */, + A3A21D8D2C8773E800600C2C /* RuntimeConfigMapView.swift in Sources */, A3422DC92C3562B700A3D867 /* Location.swift in Sources */, A36566032C54600C00C2E748 /* LoginView.swift in Sources */, A365660C2C556FF500C2E748 /* LocationsView.swift in Sources */, diff --git a/upvpn-apple/UpVPN/iOS/Views/MainViewContent.swift b/upvpn-apple/UpVPN/iOS/Views/MainViewContent.swift index 8119b43..5e68bac 100644 --- a/upvpn-apple/UpVPN/iOS/Views/MainViewContent.swift +++ b/upvpn-apple/UpVPN/iOS/Views/MainViewContent.swift @@ -57,14 +57,31 @@ struct MainViewContentNew: View { var body: some View { TabView(selection: $selectedTab) { NavigationStack { - LocationsView() + if #available(iOS 17, *), UIDevice.current.userInterfaceIdiom == .pad { + NavigationSplitView { + LocationsView(showMapInToolbar: UIDevice.current.userInterfaceIdiom == .phone) + } detail: { + LocationsMapView( + coordinateSpan: .large + ) + // to remove transparent background bar on top + .toolbarBackground(.hidden, for: .automatic) + } + // hack: otherwise searchbox shows up on toolbar + .searchable(text: .constant(""), placement: .sidebar) .navigationTitle("Locations") + // hack: otherwise an empty toolbar (and navigation title) with big height shows up. + .toolbar(.hidden) + } else { + LocationsView(showMapInToolbar: UIDevice.current.userInterfaceIdiom == .phone) + .navigationTitle("Locations") + } } .tabItem { Label("Locations", systemImage: "location") }.tag(1) - HomeView(onStatsTap: { showInspector.toggle() }) + ResponsiveHomeView(onStatsTap: { showInspector.toggle() }) .modifier(InspectorModifier(showInspector: $showInspector)) .tabItem { Label("Home", systemImage: "house") @@ -114,5 +131,10 @@ struct MainViewContent : View { #Preview { - MainViewContent() + let locationViewModel = LocationViewModel(dataRepository: DataRepository.shared, isDisconnected: {return true}) + + return MainViewContent() + .environmentObject(TunnelViewModel()) + .environmentObject(AuthViewModel(dataRepository: DataRepository.shared, isDisconnected: {return true})) + .environmentObject(locationViewModel) } diff --git a/upvpn-apple/UpVPN/iOS/Views/ResponsiveHomeView.swift b/upvpn-apple/UpVPN/iOS/Views/ResponsiveHomeView.swift new file mode 100644 index 0000000..04df0e6 --- /dev/null +++ b/upvpn-apple/UpVPN/iOS/Views/ResponsiveHomeView.swift @@ -0,0 +1,209 @@ +// +// ResponsiveView.swift +// UpVPNiOS +// +// Created by Himanshu on 9/3/24. +// + +import SwiftUI +import CoreGraphics + +extension UIApplication { + public static var isSplitOrSlideOver: Bool { + guard let screen = UIApplication.shared.connectedScenes.first as? UIWindowScene else { + return false + } + return screen.windows.first?.frame.size != screen.screen.bounds.size + } +} + +enum LayoutShape { + case phone + case padPortrait + case padLandscape +} + +private func getLayoutShape(size: CGSize) -> LayoutShape { + var shape = LayoutShape.phone + let isPad = UIDevice.current.userInterfaceIdiom == .pad + let isLandscape = size.width > size.height + + // when split or sideover is wide enough to show landscape + if UIApplication.isSplitOrSlideOver && isPad && isLandscape && size.width > 600 { + shape = LayoutShape.padLandscape + } + + // full screen on iPad + if !UIApplication.isSplitOrSlideOver && isPad { + if size.width > size.height { + shape = .padLandscape + } else { + shape = .padPortrait + } + } + + return shape +} + +enum MapOrConfig { + case map + case config +} + +struct ResponsiveHomeView: View { + + @EnvironmentObject private var tunnelViewModel: TunnelViewModel + @EnvironmentObject private var authViewModel: AuthViewModel + @EnvironmentObject private var locationViewModel: LocationViewModel + + @State private var showMapOrConfig = MapOrConfig.map + + var onStatsTap: () -> Void = {} + + var body: some View { + GeometryReader { reader in + + let layoutShape = getLayoutShape(size: reader.size) + + switch layoutShape { + case .phone: + HomeView(onStatsTap: onStatsTap) + case .padLandscape: + HStack(spacing: 0) { + HomeView(onStatsTap: { showMapOrConfig = MapOrConfig.config }) + .frame(maxWidth: 500) + + RuntimeConfigMapView(showMapOrConfig: $showMapOrConfig, + mapModifier: LandscapeMapModifier(), + configModifier: LandscapeRuntimeConfigModifier(), + pickerModifier: LandscapePickerModifier()) + .background(Color.uSystemGroupedBackground) + } + case .padPortrait: + VStack(spacing: 0) { + HStack(spacing: 0) { + HomeCard(tunnelStatus: tunnelViewModel.tunnelObserver.tunnelStatus, + selectedLocation: locationViewModel.selected, + start: { + if let location = locationViewModel.selected { + locationViewModel.addRecent(location: location) + tunnelViewModel.start(to: location) + } + }, + stop: { + tunnelViewModel.stop() + }) + .padding([.leading, .top, .bottom]) + .frame(width: reader.size.width / 2) + .aspectRatio(1, contentMode: .fit) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .background(Color.uSystemGroupedBackground) + + + if tunnelViewModel.tunnelObserver.tunnelStatus.isConnected(), + let runtimeConfig = tunnelViewModel.tunnelObserver.runtimeConfig, + let peer = runtimeConfig.peers.first, + let tx = peer.txBytes, + let rx = peer.rxBytes + + { + CardContainer { + StatsCard(tx: prettyBytes(tx), rx: prettyBytes(rx)) + } +// .contentShape(Rectangle()) + .frame(width: reader.size.width / 2) + .aspectRatio(1, contentMode: .fit) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .background(Color.uSystemGroupedBackground) + .onTapGesture { + showMapOrConfig = MapOrConfig.config + } + } else { + + if locationViewModel.recentLocations.isEmpty { + CardContainer { + WelcomeView(showSpinnner: false) + } + .frame(width: reader.size.width / 2) + .aspectRatio(1, contentMode: .fit) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .background(Color.uSystemGroupedBackground) + } else { + RecentLocationsCard() + .frame(width: reader.size.width / 2) + .aspectRatio(1, contentMode: .fit) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .background(Color.uSystemGroupedBackground) + } + } + + } + + RuntimeConfigMapView(showMapOrConfig: $showMapOrConfig, + mapModifier: PortraitMapModifier(), + configModifier: PortraitRuntimeConfigurationModifier(), + pickerModifier: PortraitPickerModifier()) + .background(Color.uSystemGroupedBackground) + } + } + } + .frame(minWidth: 350) + } +} + + +struct LandscapeMapModifier: ViewModifier { + func body(content: Content) -> some View { + content + .ignoresSafeArea(edges: .top) + .padding(.top, 5) + .padding([.trailing]) + } +} + +struct LandscapeRuntimeConfigModifier: ViewModifier { + func body(content: Content) -> some View { + content + .padding(.leading, -15) + .padding(.top, -5) + } +} + +struct PortraitMapModifier: ViewModifier { + func body(content: Content) -> some View { + content + .padding(.horizontal) + } +} + +struct PortraitRuntimeConfigurationModifier: ViewModifier { + func body(content: Content) -> some View { + content + } +} + + +struct LandscapePickerModifier: ViewModifier { + func body(content: Content) -> some View { + content + .padding([.top, .trailing, .bottom]) + } +} + +struct PortraitPickerModifier: ViewModifier { + func body(content: Content) -> some View { + content + .padding() + + } +} + + +#Preview { + let locationViewModel = LocationViewModel(dataRepository: DataRepository.shared, isDisconnected: {return true}) + + return ResponsiveHomeView() + .environmentObject(TunnelViewModel()) + .environmentObject(AuthViewModel(dataRepository: DataRepository.shared, isDisconnected: {return true})) + .environmentObject(locationViewModel) +} diff --git a/upvpn-apple/UpVPN/iOS/Views/RuntimeConfigMapView.swift b/upvpn-apple/UpVPN/iOS/Views/RuntimeConfigMapView.swift new file mode 100644 index 0000000..68fdc75 --- /dev/null +++ b/upvpn-apple/UpVPN/iOS/Views/RuntimeConfigMapView.swift @@ -0,0 +1,55 @@ +// +// RuntimeConfigMapView.swift +// UpVPNiOS +// +// Created by Himanshu on 9/3/24. +// + +import SwiftUI + +struct RuntimeConfigMapView: View { + + @Binding var showMapOrConfig: MapOrConfig + + var mapModifier: MapModifier + var configModifier: ConfigModifier + var pickerModifier: PickerModifier + + var body: some View { + if #available(iOS 17, *) { + VStack { + switch showMapOrConfig { + case .map: + LocationsMapView() + .clipShape(RoundedRectangle(cornerRadius: 10)) + .modifier(mapModifier) + case .config: + RuntimeConfigurationView() + .modifier(configModifier) + } + } + .safeAreaInset(edge: .bottom) { + Picker("", selection: $showMapOrConfig) { + Text("Runtime Configuration") + .tag(MapOrConfig.config) + + Text("Map") + .tag(MapOrConfig.map) + } + .pickerStyle(.segmented) + .modifier(pickerModifier) + } + } else { + RuntimeConfigurationView() + } + } +} + +#Preview { + RuntimeConfigMapView(showMapOrConfig: .constant(.map), + mapModifier: LandscapeMapModifier(), + configModifier: LandscapeRuntimeConfigModifier(), + pickerModifier: LandscapePickerModifier()) +}