Tutorial Sheet (incl. Welcome message + Recordings introduction)

This commit is contained in:
relikd
2020-04-17 23:37:03 +02:00
parent b44fd788b5
commit 70508c1325
12 changed files with 545 additions and 102 deletions

View File

@@ -22,9 +22,12 @@
543CDB2523EEE61900B7F323 /* GlassVPN.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 543CDB1D23EEE61900B7F323 /* GlassVPN.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
544C95262407B1C700AB89D0 /* SharedState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 544C95252407B1C700AB89D0 /* SharedState.swift */; };
5458EBC0243A3F2200CFEB15 /* TVCRecordingDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5458EBBF243A3F2200CFEB15 /* TVCRecordingDetails.swift */; };
545DDDCF243E6267003B6544 /* TutorialSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DDDCE243E6267003B6544 /* TutorialSheet.swift */; };
545DDDD124436983003B6544 /* QuickUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DDDD024436983003B6544 /* QuickUI.swift */; };
545DDDD424466D37003B6544 /* AutoLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DDDD324466D37003B6544 /* AutoLayout.swift */; };
546063E523FEFAFE008F505A /* SQDB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B7562223D7B2DC008F0C41 /* SQDB.swift */; };
54751E512423955100168273 /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54751E502423955000168273 /* FileManager.swift */; };
54751E522423955100168273 /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54751E502423955000168273 /* FileManager.swift */; };
54751E512423955100168273 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54751E502423955000168273 /* URL.swift */; };
54751E522423955100168273 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54751E502423955000168273 /* URL.swift */; };
54953E3323DC752E0054345C /* SQDB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B7562223D7B2DC008F0C41 /* SQDB.swift */; };
54953E5F23DEBE840054345C /* TVCDomains.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54953E5E23DEBE840054345C /* TVCDomains.swift */; };
54953E6123E0D69A0054345C /* TVCHosts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54953E6023E0D69A0054345C /* TVCHosts.swift */; };
@@ -166,7 +169,10 @@
543CDB2223EEE61900B7F323 /* GlassVPN.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GlassVPN.entitlements; sourceTree = "<group>"; };
544C95252407B1C700AB89D0 /* SharedState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedState.swift; sourceTree = "<group>"; };
5458EBBF243A3F2200CFEB15 /* TVCRecordingDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCRecordingDetails.swift; sourceTree = "<group>"; };
54751E502423955000168273 /* FileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManager.swift; sourceTree = "<group>"; };
545DDDCE243E6267003B6544 /* TutorialSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TutorialSheet.swift; sourceTree = "<group>"; };
545DDDD024436983003B6544 /* QuickUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickUI.swift; sourceTree = "<group>"; };
545DDDD324466D37003B6544 /* AutoLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoLayout.swift; sourceTree = "<group>"; };
54751E502423955000168273 /* URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = "<group>"; };
548B1F9423D338EC005B047C /* main.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = main.entitlements; sourceTree = "<group>"; };
54953E5E23DEBE840054345C /* TVCDomains.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCDomains.swift; sourceTree = "<group>"; };
54953E6023E0D69A0054345C /* TVCHosts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCHosts.swift; sourceTree = "<group>"; };
@@ -340,10 +346,10 @@
children = (
54B3459A2415651C004C53CC /* DB */,
54B345A4241BB975004C53CC /* Extensions */,
545DDDD224436A03003B6544 /* Common Classes */,
548B1F9423D338EC005B047C /* main.entitlements */,
541AC5D72399498A00A769D7 /* AppDelegate.swift */,
542E2A972404973F001462DC /* TBCMain.swift */,
54B34597240F18DD004C53CC /* TVC Extensions */,
540C6454240D5BAE00E948F9 /* Requests */,
540E677E242D2CD200871BBE /* Recordings */,
540C6455240D5BD200E948F9 /* Settings */,
@@ -380,12 +386,14 @@
path = GlassVPN;
sourceTree = "<group>";
};
54B34597240F18DD004C53CC /* TVC Extensions */ = {
545DDDD224436A03003B6544 /* Common Classes */ = {
isa = PBXGroup;
children = (
545DDDD024436983003B6544 /* QuickUI.swift */,
545DDDCE243E6267003B6544 /* TutorialSheet.swift */,
540C6456240D929300E948F9 /* EditableRows.swift */,
);
path = "TVC Extensions";
path = "Common Classes";
sourceTree = "<group>";
};
54B3459A2415651C004C53CC /* DB */ = {
@@ -406,7 +414,8 @@
54B345AC241BBB00004C53CC /* DBExtensions.swift */,
54B345AA241BBA5B004C53CC /* AlertSheet.swift */,
54B34595240F0513004C53CC /* TableView.swift */,
54751E502423955000168273 /* FileManager.swift */,
54751E502423955000168273 /* URL.swift */,
545DDDD324466D37003B6544 /* AutoLayout.swift */,
);
path = Extensions;
sourceTree = "<group>";
@@ -760,18 +769,20 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
545DDDD424466D37003B6544 /* AutoLayout.swift in Sources */,
54B345AD241BBB00004C53CC /* DBExtensions.swift in Sources */,
540E67842433FAFE00871BBE /* TVCPreviousRecords.swift in Sources */,
54B345A6241BB982004C53CC /* Notifications.swift in Sources */,
54B345AB241BBA5B004C53CC /* AlertSheet.swift in Sources */,
544C95262407B1C700AB89D0 /* SharedState.swift in Sources */,
545DDDCF243E6267003B6544 /* TutorialSheet.swift in Sources */,
54B345A9241BBA0B004C53CC /* Generic.swift in Sources */,
54B34596240F0513004C53CC /* TableView.swift in Sources */,
540E6780242D2CF100871BBE /* VCRecordings.swift in Sources */,
54953E3323DC752E0054345C /* SQDB.swift in Sources */,
54953E6123E0D69A0054345C /* TVCHosts.swift in Sources */,
540C6457240D929300E948F9 /* EditableRows.swift in Sources */,
54751E512423955100168273 /* FileManager.swift in Sources */,
54751E512423955100168273 /* URL.swift in Sources */,
542E2A9A24051556001462DC /* TVCSettings.swift in Sources */,
54953E5F23DEBE840054345C /* TVCDomains.swift in Sources */,
54C056DB23E9E36E00214A3F /* AppInfoType.swift in Sources */,
@@ -779,6 +790,7 @@
54C056DD23E9EEF700214A3F /* BundleIcon.swift in Sources */,
542E2A982404973F001462DC /* TBCMain.swift in Sources */,
5458EBC0243A3F2200CFEB15 /* TVCRecordingDetails.swift in Sources */,
545DDDD124436983003B6544 /* QuickUI.swift in Sources */,
541AC5D82399498A00A769D7 /* AppDelegate.swift in Sources */,
54B345992414F491004C53CC /* DBWrapper.swift in Sources */,
540E67822433483D00871BBE /* VCEditRecording.swift in Sources */,
@@ -839,7 +851,7 @@
54CA025F2426B2FD003A5E04 /* ProxyServer.swift in Sources */,
54CA02842426B2FD003A5E04 /* Rule.swift in Sources */,
54CA02B92426B2FD003A5E04 /* DirectProxySocket.swift in Sources */,
54751E522423955100168273 /* FileManager.swift in Sources */,
54751E522423955100168273 /* URL.swift in Sources */,
54CA02A92426B2FD003A5E04 /* RejectAdapter.swift in Sources */,
54CA02732426B2FD003A5E04 /* IPPool.swift in Sources */,
54CA027E2426B2FD003A5E04 /* DomainListRule.swift in Sources */,

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15705" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="sfA-EG-18J">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16096" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="sfA-EG-18J">
<device id="retina4_0" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15706"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
@@ -18,7 +18,6 @@
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</tabBar>
<connections>
<segue destination="cGm-zQ-NnO" kind="modal" identifier="welcome" id="aF0-OB-Mwx"/>
<segue destination="RcB-4v-fd4" kind="relationship" relationship="viewControllers" id="cmC-pu-5n2"/>
<segue destination="hm5-7q-Zfi" kind="relationship" relationship="viewControllers" id="pfK-BR-9lf"/>
<segue destination="dIk-JY-9vE" kind="relationship" relationship="viewControllers" id="AwW-3j-iAg"/>
@@ -28,36 +27,6 @@
</objects>
<point key="canvasLocation" x="-819" y="150"/>
</scene>
<!--Welcome Screen-->
<scene sceneID="8iq-nV-o0O">
<objects>
<viewController id="cGm-zQ-NnO" userLabel="Welcome Screen" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="FlS-lu-XEg">
<rect key="frame" x="0.0" y="0.0" width="320" height="548"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" userInteractionEnabled="NO" contentMode="scaleToFill" editable="NO" selectable="NO" id="QWn-iX-27k">
<rect key="frame" x="16" y="20" width="288" height="508"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<accessibility key="accessibilityConfiguration">
<accessibilityTraits key="traits" staticText="YES" notEnabled="YES"/>
<bool key="isElement" value="YES"/>
</accessibility>
<string key="text">Your data belongs to you. Therefore, monitoring and analysis take place on your device only. The app does not share any data with us or any other third-party.</string>
<color key="textColor" systemColor="labelColor" cocoaTouchSystemColor="darkTextColor"/>
<fontDescription key="fontDescription" name=".AppleSystemUIFont" family=".AppleSystemUIFont" pointSize="14"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<viewLayoutGuide key="safeArea" id="Gl6-Td-IRQ"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="nve-Iu-WIa" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-819" y="870"/>
</scene>
<!--Requests-->
<scene sceneID="bDO-X1-bCe">
<objects>
@@ -577,7 +546,7 @@ Duration: 60:00</string>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<sections>
<tableViewSection headerTitle="General Settings" id="w58-6X-Jea">
<tableViewSection headerTitle="VPN Proxy Settings" id="w58-6X-Jea">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="ghM-ze-fvp">
<rect key="frame" x="0.0" y="55.5" width="320" height="44"/>
@@ -608,54 +577,12 @@ Duration: 60:00</string>
</constraints>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="Qyy-0U-yhd">
<rect key="frame" x="0.0" y="99.5" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Qyy-0U-yhd" id="Mfs-fu-W5k">
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="9Ko-sD-7x0">
<rect key="frame" x="125" y="7" width="70" height="30"/>
<state key="normal" title="Export DB"/>
<connections>
<action selector="exportDB:" destination="qdB-ZO-LHY" eventType="touchUpInside" id="3gu-WF-3Xa"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="9Ko-sD-7x0" firstAttribute="centerX" secondItem="Mfs-fu-W5k" secondAttribute="centerX" id="LzG-xg-XTg"/>
<constraint firstItem="9Ko-sD-7x0" firstAttribute="centerY" secondItem="Mfs-fu-W5k" secondAttribute="centerY" id="SXw-dC-2kl"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="wzU-8s-HGb">
<rect key="frame" x="0.0" y="143.5" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="wzU-8s-HGb" id="aNM-6U-bho">
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="S6B-i8-CoC">
<rect key="frame" x="111.5" y="7" width="97" height="30"/>
<state key="normal" title="Delete all logs"/>
<connections>
<action selector="clearDatabaseResults:" destination="qdB-ZO-LHY" eventType="touchUpInside" id="w0d-8F-GmN"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="S6B-i8-CoC" firstAttribute="centerY" secondItem="aNM-6U-bho" secondAttribute="centerY" id="Wet-iT-mke"/>
<constraint firstItem="S6B-i8-CoC" firstAttribute="centerX" secondItem="aNM-6U-bho" secondAttribute="centerX" id="qM6-0t-1m4"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection headerTitle="Logging Filter" id="EcH-KA-eLE">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="detailDisclosureButton" indentationWidth="10" reuseIdentifier="settingsIgnoredCell" textLabel="UdM-Zm-G9p" detailTextLabel="bHb-Tw-nPR" style="IBUITableViewCellStyleValue2" id="fZR-we-Y0k">
<rect key="frame" x="0.0" y="243.5" width="320" height="44"/>
<rect key="frame" x="0.0" y="155.5" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="fZR-we-Y0k" id="eqc-fj-p0d">
<rect key="frame" x="0.0" y="0.0" width="261" height="44"/>
@@ -682,7 +609,7 @@ Duration: 60:00</string>
</connections>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="detailDisclosureButton" indentationWidth="10" reuseIdentifier="settingsBlockedCell" textLabel="fI0-Nt-Ucf" detailTextLabel="CGG-47-cdc" style="IBUITableViewCellStyleValue2" id="3pw-7c-M6R">
<rect key="frame" x="0.0" y="287.5" width="320" height="44"/>
<rect key="frame" x="0.0" y="199.5" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="3pw-7c-M6R" id="Smv-n1-917">
<rect key="frame" x="0.0" y="0.0" width="261" height="44"/>
@@ -710,6 +637,75 @@ Duration: 60:00</string>
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection headerTitle="Other Settings" id="wLR-T2-Qxm">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="Qyy-0U-yhd">
<rect key="frame" x="0.0" y="299.5" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Qyy-0U-yhd" id="Mfs-fu-W5k">
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="9Ko-sD-7x0">
<rect key="frame" x="125" y="7" width="70" height="30"/>
<state key="normal" title="Export DB"/>
<connections>
<action selector="exportDB:" destination="qdB-ZO-LHY" eventType="touchUpInside" id="3gu-WF-3Xa"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="9Ko-sD-7x0" firstAttribute="centerX" secondItem="Mfs-fu-W5k" secondAttribute="centerX" id="LzG-xg-XTg"/>
<constraint firstItem="9Ko-sD-7x0" firstAttribute="centerY" secondItem="Mfs-fu-W5k" secondAttribute="centerY" id="SXw-dC-2kl"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="wzU-8s-HGb">
<rect key="frame" x="0.0" y="343.5" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="wzU-8s-HGb" id="aNM-6U-bho">
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="S6B-i8-CoC">
<rect key="frame" x="74.5" y="7" width="171" height="30"/>
<state key="normal" title="Reset Introduction Alerts"/>
<connections>
<action selector="resetTutorialAlerts:" destination="qdB-ZO-LHY" eventType="touchUpInside" id="0GX-Ko-bk2"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="S6B-i8-CoC" firstAttribute="centerY" secondItem="aNM-6U-bho" secondAttribute="centerY" id="Wet-iT-mke"/>
<constraint firstItem="S6B-i8-CoC" firstAttribute="centerX" secondItem="aNM-6U-bho" secondAttribute="centerX" id="qM6-0t-1m4"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="a9C-Qy-pOf">
<rect key="frame" x="0.0" y="387.5" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="a9C-Qy-pOf" id="cUk-4x-Weg">
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="17e-nR-aCh">
<rect key="frame" x="111.5" y="7" width="97" height="30"/>
<state key="normal" title="Delete all logs">
<color key="titleColor" systemColor="systemRedColor" red="1" green="0.23137254900000001" blue="0.18823529410000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<connections>
<action selector="clearDatabaseResults:" destination="qdB-ZO-LHY" eventType="touchUpInside" id="Rep-Do-4OQ"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="17e-nR-aCh" firstAttribute="centerX" secondItem="cUk-4x-Weg" secondAttribute="centerX" id="dU5-1x-ETF"/>
<constraint firstItem="17e-nR-aCh" firstAttribute="centerY" secondItem="cUk-4x-Weg" secondAttribute="centerY" id="nLq-yi-u2E"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
</cells>
</tableViewSection>
</sections>
<connections>
<outlet property="dataSource" destination="qdB-ZO-LHY" id="RH3-xR-dpC"/>
@@ -772,12 +768,12 @@ Duration: 60:00</string>
<point key="canvasLocation" x="1400" y="150"/>
</scene>
</scenes>
<inferredMetricsTieBreakers>
<segue reference="EzT-Xq-wka"/>
</inferredMetricsTieBreakers>
<resources>
<image name="journal" width="25" height="25"/>
<image name="settings" width="25" height="25"/>
<image name="tag" width="25" height="25"/>
</resources>
<inferredMetricsTieBreakers>
<segue reference="EzT-Xq-wka"/>
</inferredMetricsTieBreakers>
</document>

View File

@@ -0,0 +1,67 @@
import UIKit
struct QuickUI {
static func button(_ title: String, target: Any? = nil, action: Selector? = nil) -> UIButton {
let x = UIButton(type: .roundedRect)
x.setTitle(title, for: .normal)
x.titleLabel?.font = .preferredFont(forTextStyle: .body)
x.sizeToFit()
if let a = action { x.addTarget(target, action: a, for: .touchUpInside) }
if #available(iOS 10.0, *) {
x.titleLabel?.adjustsFontForContentSizeCategory = true
}
return x
}
static func image(_ img: UIImage?, frame: CGRect = CGRect.zero) -> UIImageView {
let x = UIImageView(frame: frame)
x.contentMode = .scaleAspectFit
x.image = img
return x
}
static func text(_ str: String, frame: CGRect = CGRect.zero) -> UITextView {
let x = UITextView(frame: frame)
x.font = .preferredFont(forTextStyle: .body) // .systemFont(ofSize: UIFont.systemFontSize)
x.isSelectable = false
x.isEditable = false
x.text = str
if #available(iOS 10.0, *) {
x.adjustsFontForContentSizeCategory = true
}
return x
}
static func text(attributed: NSAttributedString, frame: CGRect = CGRect.zero) -> UITextView {
let txt = self.text("", frame: frame)
txt.attributedText = attributed
return txt
}
}
extension NSMutableAttributedString {
static private var def: UIFont = .preferredFont(forTextStyle: .body)
func normal(_ str: String, _ style: UIFont.TextStyle = .body) -> Self { append(str, withFont: .preferredFont(forTextStyle: style)) }
func bold(_ str: String, _ style: UIFont.TextStyle = .body) -> Self { append(str, withFont: UIFont.preferredFont(forTextStyle: style).bold()) }
func italic(_ str: String, _ style: UIFont.TextStyle = .body) -> Self { append(str, withFont: UIFont.preferredFont(forTextStyle: style).italic()) }
func h1(_ str: String) -> Self { normal(str, .title1) }
func h2(_ str: String) -> Self { normal(str, .title2) }
func h3(_ str: String) -> Self { normal(str, .title3) }
private func append(_ str: String, withFont: UIFont) -> Self {
append(NSAttributedString(string: str, attributes: [.font : withFont]))
return self
}
}
extension UIFont {
func withTraits(traits: UIFontDescriptor.SymbolicTraits) -> UIFont {
UIFont(descriptor: fontDescriptor.withSymbolicTraits(traits)!, size: 0) // keep size as is
}
func bold() -> UIFont { withTraits(traits: .traitBold) }
func italic() -> UIFont { withTraits(traits: .traitItalic) }
}

View File

@@ -0,0 +1,199 @@
import UIKit
fileprivate let margin: CGFloat = 20
fileprivate let cornerRadius: CGFloat = 15
fileprivate let uniRect = CGRect(x: 0, y: 0, width: 500, height: 500)
class TutorialSheet: UIViewController, UIScrollViewDelegate {
public var buttonTitleNext: String = "Next"
public var buttonTitleDone: String = "Close"
private var priorIndex: Int?
private var lastAnchor: NSLayoutConstraint?
private var shouldAnimate: Bool = true
private var shouldCloseBlock: (() -> Bool)? = nil
private var didCloseBlock: (() -> Void)? = nil
private let sheetBg: UIView = {
let x = UIView(frame: uniRect)
x.autoresizingMask = [.flexibleWidth, .flexibleHeight]
x.backgroundColor = .sysBg
x.layer.cornerRadius = cornerRadius
x.layer.shadowColor = UIColor.black.cgColor
x.layer.shadowRadius = 10
x.layer.shadowOpacity = 0.75
x.layer.shadowOffset = CGSize(width: 0, height: 4)
return x
}()
private let pager: UIPageControl = {
let x = UIPageControl(frame: uniRect)
x.frame.size.height = x.size(forNumberOfPages: 1).height
x.currentPageIndicatorTintColor = UIColor.sysFg.withAlphaComponent(0.5)
x.pageIndicatorTintColor = UIColor.sysFg.withAlphaComponent(0.25)
x.numberOfPages = 0
x.hidesForSinglePage = true
x.addTarget(self, action: #selector(pagerDidChange), for: .valueChanged)
return x
}()
private let pageScroll: UIScrollView = {
let x = UIScrollView(frame: uniRect)
x.bounces = false
x.isPagingEnabled = true
x.showsVerticalScrollIndicator = false
x.showsHorizontalScrollIndicator = false
let content = UIView()
x.addSubview(content)
content.translatesAutoresizingMaskIntoConstraints = false
content.anchor([.left, .right, .top, .bottom], to: x)
content.anchor([.width, .height], to: x) | .defaultLow
return x
}()
private let button: UIButton = {
let x = QuickUI.button("", target: self, action: #selector(buttonTapped))
x.contentEdgeInsets = UIEdgeInsets(all: 8)
return x
}()
// MARK: Init
required init?(coder: NSCoder) { super.init(coder: coder) }
required init() {
super.init(nibName: nil, bundle: nil)
view = makeControlUI()
modalPresentationStyle = .custom
if #available(iOS 13.0, *) {
isModalInPresentation = true
}
UIDevice.orientationDidChangeNotification.observe(call: #selector(didChangeOrientation), on: self)
}
/// Present Tutorial Sheet Controller
/// - Parameter viewController: If set to `nil`, use main application as canvas. (Default: `nil`)
/// - Parameter animate: Use `present` and `dismiss` animations. (Default: `true`)
/// - Parameter shouldClose: Called before the view controller is dismissed. Return `false` to prevent the dismissal.
/// Use this block to extract user data from input fields. (Default: `nil`)
/// - Parameter didClose: Called after the view controller is completely dismissed (with animations).
/// Use this block to update UI and visible changes. (Default: `nil`)
func present(in viewController: UIViewController? = nil, animate: Bool = true, shouldClose: (() -> Bool)? = nil, didClose: (() -> Void)? = nil) {
guard let vc = viewController ?? UIApplication.shared.keyWindow?.rootViewController else {
return
}
shouldCloseBlock = shouldClose
didCloseBlock = didClose
shouldAnimate = animate
vc.present(self, animated: animate)
}
// MARK: Dynamic UI
@discardableResult func addSheet(_ closure: ((UIStackView) -> Void)? = nil) -> UIStackView {
pager.numberOfPages += 1
updateButtonTitle()
let x = UIStackView(frame: pageScroll.bounds)
x.translatesAutoresizingMaskIntoConstraints = false
x.axis = .vertical
x.backgroundColor = UIColor.black
x.isOpaque = true
guard let content = pageScroll.subviews.first else {
return x
}
let prev = content.subviews.last
content.addSubview(x)
x.anchor([.top, .width, .height], to: pageScroll)
x.leadingAnchor =&= (prev==nil ? content.leadingAnchor : prev!.trailingAnchor)
lastAnchor?.isActive = false
lastAnchor = (x.trailingAnchor =&= pageScroll.trailingAnchor)
closure?(x)
return x
}
// MARK: Static UI
private func makeControlUI() -> UIView {
pageScroll.delegate = self
sheetBg.addSubview(pager)
sheetBg.addSubview(pageScroll)
sheetBg.addSubview(button)
for x in sheetBg.subviews { x.translatesAutoresizingMaskIntoConstraints = false }
pager.anchor([.top, .left, .right], to: sheetBg)
pageScroll.topAnchor =&= pager.bottomAnchor
pageScroll.anchor([.left, .right, .top, .bottom], to: sheetBg, margin: cornerRadius/2) | .defaultHigh
button.topAnchor =&= pageScroll.bottomAnchor
button.anchor([.bottom, .centerX], to: sheetBg)
// button.bottomAnchor =&= sheetBg.bottomAnchor - 30
// button.centerXAnchor =&= sheetBg.centerXAnchor
let bg = UIView(frame: uniRect)
bg.autoresizingMask = [.flexibleWidth, .flexibleHeight]
bg.addSubview(sheetBg)
let h: CGFloat = UIApplication.shared.isStatusBarHidden ? 0 : UIApplication.shared.statusBarFrame.height
sheetBg.frame = bg.frame.inset(by: UIEdgeInsets(all: margin, top: margin + h))
return bg
}
// MARK: Delegates
override func viewWillLayoutSubviews() {
priorIndex = pager.currentPage
}
@objc private func didChangeOrientation() {
if let i = priorIndex {
priorIndex = nil
switchToSheet(i, animated: false)
}
for case let x as UIStackView in pageScroll.subviews.first!.subviews {
x.axis = (x.frame.width > x.frame.height) ? .horizontal : .vertical
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let w = scrollView.frame.width
let new = Int((scrollView.contentOffset.x + w/2) / w)
if pager.currentPage != new {
pager.currentPage = new
updateButtonTitle()
}
}
@objc private func pagerDidChange(sender: UIPageControl) {
switchToSheet(sender.currentPage, animated: true)
}
private func switchToSheet(_ i: Int, animated: Bool) {
pageScroll.setContentOffset(CGPoint(x: CGFloat(i) * pageScroll.bounds.width, y: 0), animated: animated)
}
private func updateButtonTitle() {
let last = (pager.currentPage == pager.numberOfPages - 1)
let title = last ? buttonTitleDone : buttonTitleNext
if button.title(for: .normal) != title {
button.setTitle(title, for: .normal)
}
}
@objc private func buttonTapped() {
let next = pager.currentPage + 1
if next < pager.numberOfPages {
switchToSheet(next, animated: true)
} else {
if shouldCloseBlock?() ?? true {
dismiss(animated: shouldAnimate, completion: didCloseBlock)
}
}
}
}

View File

@@ -0,0 +1,82 @@
import UIKit
/*
Readable Auto Layout Constraints
Usage:
A.anchor =&= multiplier * B.anchor + constant | priority
*/
infix operator =&= : AdditionPrecedence
infix operator =<= : AdditionPrecedence
infix operator =>= : AdditionPrecedence
//infix operator | : AdditionPrecedence
/// Create and activate an `equal` constraint between left and right anchor. Format: `A.anchor =&= multiplier * B.anchor + constant | priority`
@discardableResult func =&= <T>(l: NSLayoutAnchor<T>, r: NSLayoutAnchor<T>) -> NSLayoutConstraint { l.constraint(equalTo: r).on() }
/// Create and activate a `lessThan` constraint between left and right anchor. Format: `A.anchor =<= multiplier * B.anchor + constant | priority`
@discardableResult func =<= <T>(l: NSLayoutAnchor<T>, r: NSLayoutAnchor<T>) -> NSLayoutConstraint { l.constraint(lessThanOrEqualTo: r).on() }
/// Create and activate a `greaterThan` constraint between left and right anchor. Format: `A.anchor =>= multiplier * B.anchor + constant | priority`
@discardableResult func =>= <T>(l: NSLayoutAnchor<T>, r: NSLayoutAnchor<T>) -> NSLayoutConstraint { l.constraint(greaterThanOrEqualTo: r).on() }
extension NSLayoutDimension { // higher precedence, so multiply first
/// Create intermediate anchor multiplier result.
static func *(l: CGFloat, r: NSLayoutDimension) -> AnchorMultiplier { .init(anchor: r, m: l) }
}
/// Intermediate `NSLayoutConstraint` anchor with multiplier supplement
struct AnchorMultiplier {
let anchor: NSLayoutDimension, m: CGFloat
/// Create and activate an `equal` constraint between left and right anchor. Format: `A.anchor =&= multiplier * B.anchor + constant | priority`
@discardableResult static func =&=(l: NSLayoutDimension, r: Self) -> NSLayoutConstraint { l.constraint(equalTo: r.anchor, multiplier: r.m).on() }
/// Create and activate a `lessThan` constraint between left and right anchor. Format: `A.anchor =<= multiplier * B.anchor + constant | priority`
@discardableResult static func =<=(l: NSLayoutDimension, r: Self) -> NSLayoutConstraint { l.constraint(lessThanOrEqualTo: r.anchor, multiplier: r.m).on() }
/// Create and activate a `greaterThan` constraint between left and right anchor. Format: `A.anchor =>= multiplier * B.anchor + constant | priority`
@discardableResult static func =>=(l: NSLayoutDimension, r: Self) -> NSLayoutConstraint { l.constraint(greaterThanOrEqualTo: r.anchor, multiplier: r.m).on() }
}
extension NSLayoutConstraint {
/// Change `isActive`to `true` and return `self`
func on() -> Self { isActive = true; return self }
/// Change `constant`attribute and return `self`
@discardableResult static func +(l: NSLayoutConstraint, r: CGFloat) -> NSLayoutConstraint { l.constant = r; return l }
/// Change `constant` attribute and return `self`
@discardableResult static func -(l: NSLayoutConstraint, r: CGFloat) -> NSLayoutConstraint { l.constant = -r; return l }
/// Change `priority` attribute and return `self`
@discardableResult static func |(l: NSLayoutConstraint, r: UILayoutPriority) -> NSLayoutConstraint { l.priority = r; return l }
}
/*
UIView extension to generate multiple constraints at once
Usage:
child.anchor([.width, .height], to: parent) | .defaultLow
*/
extension UIView {
/// Edges that need the relation to flip arguments. For these we need to inverse the constant value and relation.
private static let inverseItem: [NSLayoutConstraint.Attribute] = [.right, .bottom, .trailing, .lastBaseline, .rightMargin, .bottomMargin, .trailingMargin]
/// Create and active constraints for provided edges. Constraints will anchor the same edge on both `self` and `other`.
/// - Parameters:
/// - edges: List of constraint attributes, e.g. `[.top, .bottom, .left, .right]`
/// - other: Instance to bind to, e.g. `UIView` or `UILayoutGuide`
/// - margin: Used as constant value. Multiplier will always be `1.0`. If you need to change the multiplier, use single constraints instead. (Default: `0`)
/// - rel: Constraint relation. (Default: `.equal`)
/// - Returns: List of created and active constraints
@discardableResult func anchor(_ edges: [NSLayoutConstraint.Attribute], to other: Any, margin: CGFloat = 0, if rel: NSLayoutConstraint.Relation = .equal) -> [NSLayoutConstraint] {
edges.map {
let (A, B) = UIView.inverseItem.contains($0) ? (other, self) : (self, other)
return NSLayoutConstraint(item: A, attribute: $0, relatedBy: rel, toItem: B, attribute: $0, multiplier: 1, constant: margin).on()
}
}
}
extension Array where Element: NSLayoutConstraint {
/// set `priority` on all elements and return same list
@discardableResult static func |(l: Self, r: UILayoutPriority) -> Self {
for x in l { x.priority = r }
return l
}
}

View File

@@ -1,4 +1,4 @@
import Foundation
import UIKit
struct QLog {
private init() {}
@@ -99,5 +99,15 @@ struct TimeFormat {
static func since(_ date: Date, millis: Bool = false) -> String {
from(Date().timeIntervalSince(date), millis: millis)
}
}
extension UIColor {
static var sysBg: UIColor { get { if #available(iOS 13.0, *) { return .systemBackground } else { return .white } }}
static var sysFg: UIColor { get { if #available(iOS 13.0, *) { return .label } else { return .black } }}
}
extension UIEdgeInsets {
init(all: CGFloat = 0, top: CGFloat? = nil, left: CGFloat? = nil, bottom: CGFloat? = nil, right: CGFloat? = nil) {
self.init(top: top ?? all, left: left ?? all, bottom: bottom ?? all, right: right ?? all)
}
}

View File

@@ -10,14 +10,10 @@ fileprivate extension FileManager {
func internalDB() -> URL {
appGroupDir().appendingPathComponent("dns-logs.sqlite")
}
func appGroupIPC() -> URL {
appGroupDir().appendingPathComponent("data-exchange.dat")
}
}
extension URL {
static func exportDir() -> URL { FileManager.default.exportDir() }
static func appGroupDir() -> URL { FileManager.default.appGroupDir() }
static func internalDB() -> URL { FileManager.default.internalDB() }
static func appGroupIPC() -> URL { FileManager.default.appGroupIPC() }
}

View File

@@ -23,6 +23,9 @@ class VCEditRecording: UIViewController, UITextFieldDelegate, UITextViewDelegate
validateSaveButton()
if deleteOnCancel { // mark as destructive
buttonCancel.tintColor = .systemRed
if #available(iOS 13.0, *) {
isModalInPresentation = true
}
}
UIResponder.keyboardWillShowNotification.observe(call: #selector(keyboardWillShow), on: self)
UIResponder.keyboardWillHideNotification.observe(call: #selector(keyboardWillHide), on: self)

View File

@@ -19,6 +19,10 @@ class VCRecordings: UIViewController, UINavigationControllerDelegate {
// hide timer if not running
updateUI(setRecording: false, animated: false)
currentRecording = DBWrp.recordingGetCurrent()
if !UserDefaults.standard.bool(forKey: "didShowTutorialRecordings") {
self.perform(#selector(showTutorial), with: nil, afterDelay: 0.5)
}
}
override func viewDidAppear(_ animated: Bool) {
@@ -86,4 +90,44 @@ class VCRecordings: UIViewController, UINavigationControllerDelegate {
self.startButton.setTitleColor(color, for: .normal)
}
}
// MARK: Tutorial View Controller
@objc private func showTutorial() {
let x = TutorialSheet()
x.addSheet().addArrangedSubview(QuickUI.text(attributed: NSMutableAttributedString()
.h1("What are Recordings?\n")
.normal("\nSimilar to the default logging, recordings will intercept every request and log it for later review. " +
"Recordings are usually 3  5 minutes long and cover a single application. " +
"You can utilize recordings for App analysis or to get a ground truth for background traffic." +
"\n\n" +
"Optionally, you can help us by providing app specific recordings. " +
"Together with your findings we can create a community driven privacy monitor. " +
"The research results will help you and others avoid Apps that unnecessarily share data with third-party providers.")
))
x.addSheet().addArrangedSubview(QuickUI.text(attributed: NSMutableAttributedString()
.h1("How to record?\n")
.normal("\nBefore you begin a new recording make sure that you quit all running applications. " +
"Tap on the 'Start Recording' button and switch to the application you'd like to inspect. " +
"Use the App as you would normally. Try to get to all corners and functionality the App provides. " +
"When you feel that you have captured enough content, come back to ").italic("AppCheck").normal(" and stop the recording." +
"\n\n" +
"Upon completion you will find your recording in the 'Previous Recordings' section. " +
"You can review your results and remove user specific information if necessary.")
))
x.addSheet().addArrangedSubview(QuickUI.text(attributed: NSMutableAttributedString()
.h1("Share results\n")
.normal("\nThis step is completely ").bold("optional").normal(". " +
"You can choose to share your results with us. " +
"We can compare similar applications and suggest privacy friendly alternatives. " +
"Together with other likeminded individuals we can increase the awareness for privacy friendly design." +
"\n\n" +
"Thank you very much.")
))
x.buttonTitleDone = "Got it"
x.present {
UserDefaults.standard.set(true, forKey: "didShowTutorialRecordings")
}
}
}

View File

@@ -52,11 +52,18 @@ class TVCSettings: UITableViewController {
// }.presentIn(self)
}
@IBAction func resetTutorialAlerts(_ sender: UIButton) {
UserDefaults.standard.removeObject(forKey: "didShowTutorialAppWelcome")
UserDefaults.standard.removeObject(forKey: "didShowTutorialRecordings")
Alert(title: sender.titleLabel?.text,
text: "\nDone.\n\nYou may need to restart the application.").presentIn(self)
}
@IBAction func clearDatabaseResults(_ sender: Any) {
AskAlert(title: "Clear results?", text: """
You are about to delete all results that have been logged in the past. Your preference for blocked and ignored domains is preserved.
Continue?
""", buttonText: "Delete", buttonStyle: .destructive) { _ in
AskAlert(title: "Clear results?", text:
"You are about to delete all results that have been logged in the past. " +
"Your preferences for blocked and ignored domains are preserved.\n" +
"Continue?", buttonText: "Delete", buttonStyle: .destructive) { _ in
DBWrp.deleteHistory()
}.presentIn(self)
}

View File

@@ -5,13 +5,40 @@ class TBCMain: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
// perform(#selector(showWelcomeMessage), with: nil, afterDelay: 3)
NotifyVPNStateChanged.observe(call: #selector(vpnStateChanged(_:)), on: self)
changedState(currentVPNState)
if !UserDefaults.standard.bool(forKey: "didShowTutorialAppWelcome") {
self.perform(#selector(showWelcomeMessage), with: nil, afterDelay: 0.5)
}
}
@objc func showWelcomeMessage() {
performSegue(withIdentifier: "welcome", sender: nil)
let x = TutorialSheet()
x.addSheet().addArrangedSubview(QuickUI.text(attributed: NSMutableAttributedString()
.h1("Welcome\n")
.normal("\nAppCheck helps you identify which applications communicate with third parties. " +
"It does so by logging network requests. " +
"AppCheck learns only the destination addresses, not the actual data that is exchanged." +
"\n\n" +
"Your data belongs to you. " +
"Therefore, monitoring and analysis take place on your device only. " +
"The app does not share any data with us or any other third-party. " +
"Unless you choose to.")
))
x.addSheet().addArrangedSubview(QuickUI.text(attributed: NSMutableAttributedString()
.h1("How it works\n")
.normal("\nAppCheck creates a local VPN tunnel to intercept all network connections. " +
"For each connection AppCheck looks into the DNS headers only, namely the domain names. " +
"\n" +
"These domain names are logged in the background while the VPN is active. " +
"That means, AppCheck does not have to be active in the foreground. " +
"You can close the app and come back later to see the results."
)
))
x.present {
UserDefaults.standard.set(true, forKey: "didShowTutorialAppWelcome")
}
}
@objc func vpnStateChanged(_ notification: Notification) {