From 3d68abe25bfffdf9c1026d28bb9b24b46894106b Mon Sep 17 00:00:00 2001 From: Norbel AMBANUMBEN Date: Fri, 28 Jun 2024 12:00:31 +0100 Subject: [PATCH 1/5] feat: update overview screen --- ooniprobe.xcodeproj/project.pbxproj | 10 +- ooniprobe/Storyboards/Dashboard.storyboard | 43 ++- ...TestOverviewViewController+TableView.swift | 295 ++++++++++++++++++ .../View/RunTest/TestOverviewViewController.h | 5 +- .../View/RunTest/TestOverviewViewController.m | 6 +- ooniprobe/ooniprobe-Bridging-Header.h | 1 + 6 files changed, 339 insertions(+), 21 deletions(-) create mode 100644 ooniprobe/View/RunTest/TestOverviewViewController+TableView.swift diff --git a/ooniprobe.xcodeproj/project.pbxproj b/ooniprobe.xcodeproj/project.pbxproj index b8cc63cc..fe883dc4 100644 --- a/ooniprobe.xcodeproj/project.pbxproj +++ b/ooniprobe.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -12,13 +12,14 @@ 17E7EDC021BFEE0C001961C7 /* SnapshotHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17E7EDBF21BFEE0C001961C7 /* SnapshotHelper.swift */; }; 526C702A25C99AB200C7A164 /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 526C702925C99AB100C7A164 /* Colors.xcassets */; }; 58E18F9EBF4FAD4EFCE020F3 /* Pods_ooniprobe.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BB32DF3D6FC174AA1AF76009 /* Pods_ooniprobe.framework */; }; - 793587D32B8E081600038F88 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 793587D22B8E081600038F88 /* Utils.swift */; }; 793587BA2B852EDD00038F88 /* OoniRunViewUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 793587B92B852EDD00038F88 /* OoniRunViewUITests.swift */; }; + 793587D32B8E081600038F88 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 793587D22B8E081600038F88 /* Utils.swift */; }; 7940AA8B28117E9000C0EB5D /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7940AA8A28117E9000C0EB5D /* ShareViewController.swift */; }; 7940AA8E28117E9000C0EB5D /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7940AA8C28117E9000C0EB5D /* MainInterface.storyboard */; }; 7940AA9228117E9000C0EB5D /* share.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 7940AA8828117E9000C0EB5D /* share.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 7945903D2C21BFB1008116BF /* OONIDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7945903C2C21BFB1008116BF /* OONIDescriptor.swift */; }; 79780FCF27E9F18E002A38B1 /* Languages.plist in Resources */ = {isa = PBXBuildFile; fileRef = 79780FCE27E9F18E002A38B1 /* Languages.plist */; }; + 79DB62342C2D8F020076FA0C /* TestOverviewViewController+TableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79DB62332C2D8F020076FA0C /* TestOverviewViewController+TableView.swift */; }; 7AED19812A6EC9A2003B265A /* libresolv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 7AED19802A6EC9A2003B265A /* libresolv.tbd */; }; 7AED19832A6EC9C7003B265A /* libresolv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 7AED19822A6EC9C7003B265A /* libresolv.tbd */; }; D4A2F5DF1A6C3244001B8460 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = D4A2F5DE1A6C3244001B8460 /* main.m */; }; @@ -221,8 +222,8 @@ 17E7EDBF21BFEE0C001961C7 /* SnapshotHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SnapshotHelper.swift; path = fastlane/SnapshotHelper.swift; sourceTree = SOURCE_ROOT; }; 526C702925C99AB100C7A164 /* Colors.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Colors.xcassets; sourceTree = ""; }; 588219FACC9F793A15BDEA33 /* Pods_OONIProbeUnitTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_OONIProbeUnitTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 793587D22B8E081600038F88 /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; }; 793587B92B852EDD00038F88 /* OoniRunViewUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OoniRunViewUITests.swift; sourceTree = ""; }; + 793587D22B8E081600038F88 /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; }; 7940AA8828117E9000C0EB5D /* share.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = share.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 7940AA8A28117E9000C0EB5D /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = ""; }; 7940AA8D28117E9000C0EB5D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; @@ -233,6 +234,7 @@ 79780FCE27E9F18E002A38B1 /* Languages.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Languages.plist; sourceTree = ""; }; 79AA093C2A86E44400C23E27 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/Localizable.strings; sourceTree = ""; }; 79AA093D2A86E47600C23E27 /* my */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = my; path = my.lproj/Localizable.strings; sourceTree = ""; }; + 79DB62332C2D8F020076FA0C /* TestOverviewViewController+TableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TestOverviewViewController+TableView.swift"; sourceTree = ""; }; 7A8CB0932ADDDAC1005AB2BC /* libcrypto.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = libcrypto.xcframework; path = Pods/libcrypto/libcrypto.xcframework; sourceTree = ""; }; 7A8CB0942ADDDAC1005AB2BC /* libevent.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = libevent.xcframework; path = Pods/libevent/libevent.xcframework; sourceTree = ""; }; 7A8CB0952ADDDAC1005AB2BC /* libssl.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = libssl.xcframework; path = Pods/libssl/libssl.xcframework; sourceTree = ""; }; @@ -720,6 +722,7 @@ ED0D8D6F20186742003DDF23 /* TestOverviewViewController.m */, ED21D090269C93B900BB09D8 /* ProgressViewController.h */, ED21D091269C93B900BB09D8 /* ProgressViewController.m */, + 79DB62332C2D8F020076FA0C /* TestOverviewViewController+TableView.swift */, ); path = RunTest; sourceTree = ""; @@ -1648,6 +1651,7 @@ EDA9F21E224255E7003D40E8 /* TestSummaryViewController.m in Sources */, EDFE073D1F375E3C00960AF2 /* DictionaryUtility.m in Sources */, ED2E066D21D4867B00E9B9EE /* WebConnectivity.m in Sources */, + 79DB62342C2D8F020076FA0C /* TestOverviewViewController+TableView.swift in Sources */, EDF4ED27248A9A64001A5406 /* Simple.m in Sources */, EDF4ECF4248549BD001A5406 /* Engine.m in Sources */, ED58CF7A1FEBB9A900E3C415 /* DashboardTableViewController.m in Sources */, diff --git a/ooniprobe/Storyboards/Dashboard.storyboard b/ooniprobe/Storyboards/Dashboard.storyboard index 741d4dc6..57ae7579 100644 --- a/ooniprobe/Storyboards/Dashboard.storyboard +++ b/ooniprobe/Storyboards/Dashboard.storyboard @@ -1,10 +1,11 @@ - + - + + @@ -423,14 +424,14 @@ - + diff --git a/ooniprobe/View/RunTest/TestOverviewViewController+TableView.swift b/ooniprobe/View/RunTest/TestOverviewViewController+TableView.swift new file mode 100644 index 00000000..cd855937 --- /dev/null +++ b/ooniprobe/View/RunTest/TestOverviewViewController+TableView.swift @@ -0,0 +1,295 @@ +import Foundation +import SwiftUI + +struct OverviewContentView: View { + let descriptor:OONIDescriptor + @State var runTestsAutomatically:Bool = false { + didSet { + updateNettests(runTestsAutomatically) + } + } + @State var installUpdatesAutomatically:Bool = false + + var body: some View { + HStack{ + VStack { + Text(descriptor.longDescription) + .font(.callout) + .padding() + Text("Test Settings") + Toggle("Install updates automatically", isOn: $installUpdatesAutomatically) + Toggle("Run tests automatically", isOn: $runTestsAutomatically).toggleStyle(iOSCheckboxToggleStyle()) + UITableViewWrapper(nettests: descriptor.nettest) + .frame(height: 1000) + } + } + } + + func updateNettests(_ newValue: Bool) { + // ... update nettests array ... + } +} + +extension TestOverviewViewController { + open override func viewDidAppear(_ animated: Bool) { + + let contentView = OverviewContentView(descriptor: self.descriptor as! OONIDescriptor) + + let hostingController = UIHostingController(rootView: contentView) + + addChild(hostingController) + + if let hostingView = hostingController.view { + hostingView.translatesAutoresizingMaskIntoConstraints = false + self.scrollView.addSubview(hostingView) + NSLayoutConstraint.activate([ + hostingView.leadingAnchor.constraint(equalTo: self.scrollView.leadingAnchor, constant: 20), + hostingView.trailingAnchor.constraint(equalTo: self.scrollView.trailingAnchor, constant: -20), + hostingView.topAnchor.constraint(equalTo: self.scrollView.topAnchor), + ]) + } + + } +} + +// MARK: - NettestStatus + +/// A struct that represents the status of a Nettest. +struct NettestStatus { + var nettest: Nettest + var isSelected: Bool = false + var isExpanded: Bool = false +} + +// MARK: - MarkdownLabel + +/// A SwiftUI view that displays a Markdown label. +struct MarkdownLabel: UIViewRepresentable { + var rect: CGRect + + func makeUIView(context: Context) -> RHMarkdownLabel { + return RHMarkdownLabel(frame: rect) + } + + func updateUIView(_ uiView: RHMarkdownLabel, context: Context) { + uiView.markdown = NSLocalizedString("Dashboard.InstantMessaging.Overview.Paragraph", comment: "") + } +} + +// MARK: - UITableViewWrapper + +/// A SwiftUI view that wraps a UITableView. +struct UITableViewWrapper: UIViewRepresentable { + var nettests: [NettestStatus] + + /// Initializes a new instance of the UITableViewWrapper struct. + /// - Parameter nettests: An array of Nettest objects. + init(nettests: [Nettest]) { + self.nettests = nettests.map { nettest in NettestStatus(nettest: nettest) } + } + + func makeUIView(context: Context) -> UITableView { + let tableView = UITableView() + tableView.dataSource = context.coordinator + tableView.delegate = context.coordinator + tableView.register(NettestTableViewCell.self, forCellReuseIdentifier: "nettests_cell") + tableView.register(InputTableViewCell.self, forCellReuseIdentifier: "inputs_cell") + return tableView + } + + func updateUIView(_ uiView: UITableView, context: Context) { + uiView.reloadData() + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + /// A class that conforms to the UITableViewDataSource and UITableViewDelegate protocols. + class Coordinator: NSObject, UITableViewDataSource, UITableViewDelegate { + var parent: UITableViewWrapper + + init(_ parent: UITableViewWrapper) { + self.parent = parent + } + + func numberOfSections(in tableView: UITableView) -> Int { + parent.nettests.count + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + + let section = parent.nettests[section] + if section.isExpanded { + + if let inputs = section.nettest.inputs, !inputs.isEmpty { // Check if the section(`nettest`) has inputs + return inputs.count + 1 // Return the number of inputs plus 1 (for the section header) + } else { + return 1 // Return 1 if there are no inputs (only the section header) + } + } else { + return 1 // Return 1 if the section is not expanded (only the section header) + } + } + + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + if indexPath.row == 0 { + let cell = tableView.dequeueReusableCell(withIdentifier: "nettests_cell") as! NettestTableViewCell + + cell.configure( + with: parent.nettests[indexPath.section], + onToggleChange: { [weak self] newValue in + //TODO: Save preference change to database + // Update the isSelected property of the NettestStatus object for the current section to the new value of the toggle. + self?.parent.nettests[indexPath.section].isSelected = newValue + tableView.reloadData() + } + ) + return cell + } else { + let cell = tableView.dequeueReusableCell(withIdentifier: "inputs_cell") as! InputTableViewCell + + if let inputs = parent.nettests[indexPath.section].nettest.inputs, !inputs.isEmpty { + + cell.configure(with: inputs[indexPath.row - 1]) + return cell + } else { + return cell + } + + } + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + + if indexPath.row == 0{ + parent.nettests[indexPath.section].isExpanded = !parent.nettests[indexPath.section].isExpanded + } + + UIView.transition(with: tableView, + duration: 0.35, + options: .transitionCrossDissolve, + animations: { tableView.reloadData() }) + + print("\(indexPath.section)-\(indexPath.row)") + } + } +} + + +/// A SwiftUI toggle style that uses a checkbox. +struct iOSCheckboxToggleStyle: ToggleStyle { + func makeBody(configuration: Configuration) -> some View { + Button(action: { + configuration.isOn.toggle() + }, label: { + HStack { + configuration.label + Spacer() + Image(systemName: configuration.isOn ? "checkmark.square" : "square").padding() + } + }) + } +} + +// MARK: - Nettests views and TableCell + +/// A SwiftUI view that represents a section in the table view. +struct SectionTableCell: View { + var item: NettestStatus + @Binding var isOn: Bool + + var body: some View { + HStack { + Text(LocalizationUtility.getNameForTest(item.nettest.name)).padding() + if let inputs = item.nettest.inputs, !inputs.isEmpty { + Image(systemName: item.isExpanded ? "chevron.up" : "chevron.down") + } else { + Spacer() + } + Spacer() + Toggle(isOn: $isOn) {}.toggleStyle(iOSCheckboxToggleStyle()) + } + } +} + +/// A UITableViewCell subclass that displays a section in the table view. +class NettestTableViewCell: UITableViewCell { + private var hostingController: UIHostingController? + + /// Configures the cell with the specified data. + /// - Parameters: + /// - data: The NettestStatus object. + /// - onToggleChange: A closure that is called when the toggle is changed. + func configure(with data: NettestStatus, onToggleChange: @escaping (Bool) -> Void) { + // Create a binding to pass the data to the SwiftUI view + let binding = Binding( + get: { data.isSelected }, + set: { newValue in + onToggleChange(newValue) + } + ) + + let toggleCellView = SectionTableCell(item:data, isOn: binding) + + if let hostingController = hostingController { + hostingController.rootView = toggleCellView + } else { + hostingController = UIHostingController(rootView: toggleCellView) + if let hostingView = hostingController?.view { + hostingView.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(hostingView) + NSLayoutConstraint.activate([ + hostingView.topAnchor.constraint(equalTo: contentView.topAnchor), + hostingView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + hostingView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + hostingView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + ]) + } + } + } +} + +// MARK: - Input views and TableCell + +/// A SwiftUI view that represents an input in the table view. +struct InputTableView: View { + var item: String + + var body: some View { + HStack { + Text(item).padding() + Spacer() + } + } +} + +/// A UITableViewCell subclass that displays an input in the table view. +class InputTableViewCell: UITableViewCell { + private var hostingController: UIHostingController? + + /// Configures the cell with the specified data. + /// - Parameter data: The input string. + func configure(with data: String) { + + let toggleCellView = InputTableView(item:data) + + if let hostingController = hostingController { + hostingController.rootView = toggleCellView + } else { + hostingController = UIHostingController(rootView: toggleCellView) + if let hostingView = hostingController?.view { + hostingView.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(hostingView) + NSLayoutConstraint.activate([ + hostingView.topAnchor.constraint(equalTo: contentView.topAnchor), + hostingView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + hostingView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + hostingView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + ]) + } + } + } +} diff --git a/ooniprobe/View/RunTest/TestOverviewViewController.h b/ooniprobe/View/RunTest/TestOverviewViewController.h index adbaa793..301f113b 100644 --- a/ooniprobe/View/RunTest/TestOverviewViewController.h +++ b/ooniprobe/View/RunTest/TestOverviewViewController.h @@ -12,8 +12,12 @@ UIColor *defaultColor; } ++ (void)loadSwiftUIViews; + @property (nonatomic, strong) id descriptor; +@property (weak, nonatomic) IBOutlet UIScrollView *scrollView; +@property (weak, nonatomic) IBOutlet UITableView *tableView; @property (strong, nonatomic) IBOutlet UIImageView *testImage; @property (strong, nonatomic) IBOutlet UILabel *testNameLabel; @property (strong, nonatomic) IBOutlet ConfigureButton *websitesButton; @@ -25,6 +29,5 @@ @property (strong, nonatomic) IBOutlet RHMarkdownLabel *testDescriptionLabel; @property (strong, nonatomic) IBOutlet UIView *backgroundView; @property (strong, nonatomic) IBOutlet NSLayoutConstraint *tableFooterConstraint; -@property (strong, nonatomic) IBOutlet UIScrollView *scrollView; @end diff --git a/ooniprobe/View/RunTest/TestOverviewViewController.m b/ooniprobe/View/RunTest/TestOverviewViewController.m index 36f78813..6fd80869 100644 --- a/ooniprobe/View/RunTest/TestOverviewViewController.m +++ b/ooniprobe/View/RunTest/TestOverviewViewController.m @@ -50,8 +50,12 @@ - (void)viewDidLoad { [self.backgroundView setBackgroundColor:defaultColor]; [NavigationBarUtility setNavigationBar:self.navigationController.navigationBar color:defaultColor]; self.navigationController.navigationBar.topItem.title = @""; + + [self loadSwiftUIViews]; } +- (void)loadSwiftUIViews{} + -(void)viewWillAppear:(BOOL)animated{ [super viewWillAppear:animated]; [NavigationBarUtility setBarTintColor:self.navigationController.navigationBar @@ -63,13 +67,11 @@ -(void)changeConstraints{ dispatch_async(dispatch_get_main_queue(), ^{ if ([RunningTest currentTest].isTestRunning){ self.tableFooterConstraint.constant = 64; - [self.scrollView setNeedsUpdateConstraints]; } else { //If this number is > 0 there are still test running if ([[RunningTest currentTest].testSuites count] == 0){ self.tableFooterConstraint.constant = 0; - [self.scrollView setNeedsUpdateConstraints]; } } }); diff --git a/ooniprobe/ooniprobe-Bridging-Header.h b/ooniprobe/ooniprobe-Bridging-Header.h index 99a55785..8d2eb529 100644 --- a/ooniprobe/ooniprobe-Bridging-Header.h +++ b/ooniprobe/ooniprobe-Bridging-Header.h @@ -7,3 +7,4 @@ #import "AbstractSuite.h" #import "Tests.h" #import "UIView+Toast.h" +#import "LocalizationUtility.h" From 714c201a29a2c358010c509e81759e9f97392253 Mon Sep 17 00:00:00 2001 From: Norbel AMBANUMBEN Date: Thu, 25 Jul 2024 14:34:07 +0100 Subject: [PATCH 2/5] chore: fix click events emitter for overview page --- ooniprobe/Storyboards/Dashboard.storyboard | 33 ++----- ...TestOverviewViewController+TableView.swift | 98 ++++++++++++------- 2 files changed, 74 insertions(+), 57 deletions(-) diff --git a/ooniprobe/Storyboards/Dashboard.storyboard b/ooniprobe/Storyboards/Dashboard.storyboard index 57ae7579..8937e3a7 100644 --- a/ooniprobe/Storyboards/Dashboard.storyboard +++ b/ooniprobe/Storyboards/Dashboard.storyboard @@ -424,14 +424,14 @@ - +