├── .gitignore
├── LICENSE
├── README.md
├── Wandra.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ ├── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
│ └── xcuserdata
│ │ └── mikael.xcuserdatad
│ │ └── UserInterfaceState.xcuserstate
├── xcshareddata
│ └── xcschemes
│ │ └── Wandra.xcscheme
└── xcuserdata
│ └── mikael.xcuserdatad
│ ├── xcdebugger
│ └── Breakpoints_v2.xcbkptlist
│ └── xcschemes
│ └── xcschememanagement.plist
├── Wandra
├── Assets.xcassets
│ ├── AppIcon.appiconset
│ │ ├── Contents.json
│ │ ├── wandra-ikon-1024.png
│ │ ├── wandra-ikon-128.png
│ │ ├── wandra-ikon-16.png
│ │ ├── wandra-ikon-256.png
│ │ ├── wandra-ikon-257.png
│ │ ├── wandra-ikon-32.png
│ │ ├── wandra-ikon-33.png
│ │ ├── wandra-ikon-512.png
│ │ ├── wandra-ikon-513.png
│ │ └── wandra-ikon-64.png
│ ├── Contents.json
│ ├── DarkGreen.colorset
│ │ └── Contents.json
│ ├── LightGreen.colorset
│ │ └── Contents.json
│ ├── Orange.colorset
│ │ └── Contents.json
│ ├── Red.colorset
│ │ └── Contents.json
│ └── Yellow.colorset
│ │ └── Contents.json
├── ContentView.swift
├── Functions.swift
├── HelpTexts.swift
├── Info.plist
├── Location.swift
├── Main.swift
├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
├── Wandra.entitlements
└── sonar.mp3
└── images
├── preview.png
└── wandra-ikon.png
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Mikael Löfgren
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 | # Wandra
3 | Simple Wi-Fi analyzer for macOS built in SwiftUI.
4 |
5 |
6 | It displays your Signal to Noise Ratio and Received Signal Strength.
7 | Basestation SSID and MAC-address, you can export measured data as
8 | a csv file to import to Numbers/Excel by rightclick the play button.
9 |
10 | System requirements
11 | macOS 11.0
12 |
13 | Download from here:
14 | https://github.com/mikaellofgren/wandra/releases
15 |
16 | 
17 |
18 | # Help
19 | Click the textfields in the app to bring up help texts.
20 | **Signal to Noise Ratio (SNR)**
21 | Higher value is better
22 |
23 | Its measured by taking the signal strength (RSSI)
24 | and subtracting the noise.
25 | Noise is all the RF around the radio in the spectrum
26 | from all RF sources. It can come from a microwave, bluetooth devices, etc.
27 |
28 | Wireless adapters (NIC) for laptops, tablets,
29 | aren’t capable of determining accurate SNR.
30 | An example is when a microwave is turned on.
31 | The NIC will not be able to see the RF signal
32 | because the microwave is sending unmodulated bits.
33 | Thus the built-in NIC believes there is no noise.
34 |
35 | **Received Signal Strength Indicator (RSSI)**
36 | Measured in dBm (decibel milliwatts)
37 |
38 | Lower value is better
39 | Every 3 dB lower = doubles signal strength
40 | Every 3 dB higher = halves signal strength
41 | Usually it starts around -30 near the access point,
42 | and gets higher (losing strength)
43 | when moving away from the access point.
44 |
45 | **Basic Service Set Identifiers (BSSID)**
46 | Most of the time it is associated with MAC address of the access point
47 | Stick to current BSSID until:
48 | - RSSI below -75dBm
49 | - Choose 5Ghz over 2.4GHz as long as 5GHz is more than -68dBm
50 | - Does NOT support 802.11k
51 | - Choose AP with RSSI 12dB higher than current AP
52 |
53 | If multiple 5 GHz SSIDs meet this level,
54 | macOS chooses a network based on these criteria:
55 | - 802.11ax is preferred over 802.11ac.
56 | - 802.11ac is preferred over 802.11n or 802.11a.
57 | - 802.11n is preferred over 802.11a.
58 | - 80 MHz channel width is preferred over 40 MHz or 20 MHz.
59 | - 40 MHz channel width is preferred over 20 MHz.
60 |
61 | https://support.apple.com/en-us/HT206207
62 |
--------------------------------------------------------------------------------
/Wandra.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 54;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 738642B4261A4A580098091E /* HelpTexts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 738642B3261A4A580098091E /* HelpTexts.swift */; };
11 | 73B576662B0FBA3000460D0F /* Location.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73B576652B0FBA3000460D0F /* Location.swift */; };
12 | 73EB31B325E5527000A9CAC1 /* Main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73EB31B225E5527000A9CAC1 /* Main.swift */; };
13 | 73EB31B525E5527000A9CAC1 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73EB31B425E5527000A9CAC1 /* ContentView.swift */; };
14 | 73EB31B725E5527900A9CAC1 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 73EB31B625E5527900A9CAC1 /* Assets.xcassets */; };
15 | 73EB31BA25E5527900A9CAC1 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 73EB31B925E5527900A9CAC1 /* Preview Assets.xcassets */; };
16 | 73EB31C425E6CAF500A9CAC1 /* Functions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73EB31C325E6CAF500A9CAC1 /* Functions.swift */; };
17 | 73F6F4BA262347CE005D666C /* sonar.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 73F6F4B9262347CE005D666C /* sonar.mp3 */; };
18 | /* End PBXBuildFile section */
19 |
20 | /* Begin PBXFileReference section */
21 | 738642B3261A4A580098091E /* HelpTexts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelpTexts.swift; sourceTree = ""; };
22 | 73B576652B0FBA3000460D0F /* Location.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Location.swift; sourceTree = ""; };
23 | 73C2CA442610AFD500259A75 /* NetworkExtension.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NetworkExtension.framework; path = System/Library/Frameworks/NetworkExtension.framework; sourceTree = SDKROOT; };
24 | 73EB31AF25E5527000A9CAC1 /* Wandra.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Wandra.app; sourceTree = BUILT_PRODUCTS_DIR; };
25 | 73EB31B225E5527000A9CAC1 /* Main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Main.swift; sourceTree = ""; };
26 | 73EB31B425E5527000A9CAC1 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
27 | 73EB31B625E5527900A9CAC1 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
28 | 73EB31B925E5527900A9CAC1 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
29 | 73EB31BB25E5527900A9CAC1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
30 | 73EB31BC25E5527900A9CAC1 /* Wandra.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Wandra.entitlements; sourceTree = ""; };
31 | 73EB31C325E6CAF500A9CAC1 /* Functions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Functions.swift; sourceTree = ""; };
32 | 73F6F4B9262347CE005D666C /* sonar.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = sonar.mp3; sourceTree = ""; };
33 | /* End PBXFileReference section */
34 |
35 | /* Begin PBXFrameworksBuildPhase section */
36 | 73EB31AC25E5527000A9CAC1 /* Frameworks */ = {
37 | isa = PBXFrameworksBuildPhase;
38 | buildActionMask = 2147483647;
39 | files = (
40 | );
41 | runOnlyForDeploymentPostprocessing = 0;
42 | };
43 | /* End PBXFrameworksBuildPhase section */
44 |
45 | /* Begin PBXGroup section */
46 | 73C2CA432610AFD500259A75 /* Frameworks */ = {
47 | isa = PBXGroup;
48 | children = (
49 | 73C2CA442610AFD500259A75 /* NetworkExtension.framework */,
50 | );
51 | name = Frameworks;
52 | sourceTree = "";
53 | };
54 | 73EB31A625E5527000A9CAC1 = {
55 | isa = PBXGroup;
56 | children = (
57 | 73EB31B125E5527000A9CAC1 /* Wandra */,
58 | 73EB31B025E5527000A9CAC1 /* Products */,
59 | 73C2CA432610AFD500259A75 /* Frameworks */,
60 | );
61 | sourceTree = "";
62 | };
63 | 73EB31B025E5527000A9CAC1 /* Products */ = {
64 | isa = PBXGroup;
65 | children = (
66 | 73EB31AF25E5527000A9CAC1 /* Wandra.app */,
67 | );
68 | name = Products;
69 | sourceTree = "";
70 | };
71 | 73EB31B125E5527000A9CAC1 /* Wandra */ = {
72 | isa = PBXGroup;
73 | children = (
74 | 73EB31B225E5527000A9CAC1 /* Main.swift */,
75 | 738642B3261A4A580098091E /* HelpTexts.swift */,
76 | 73EB31B425E5527000A9CAC1 /* ContentView.swift */,
77 | 73EB31C325E6CAF500A9CAC1 /* Functions.swift */,
78 | 73B576652B0FBA3000460D0F /* Location.swift */,
79 | 73EB31B625E5527900A9CAC1 /* Assets.xcassets */,
80 | 73EB31BB25E5527900A9CAC1 /* Info.plist */,
81 | 73EB31BC25E5527900A9CAC1 /* Wandra.entitlements */,
82 | 73F6F4B9262347CE005D666C /* sonar.mp3 */,
83 | 73EB31B825E5527900A9CAC1 /* Preview Content */,
84 | );
85 | path = Wandra;
86 | sourceTree = "";
87 | };
88 | 73EB31B825E5527900A9CAC1 /* Preview Content */ = {
89 | isa = PBXGroup;
90 | children = (
91 | 73EB31B925E5527900A9CAC1 /* Preview Assets.xcassets */,
92 | );
93 | path = "Preview Content";
94 | sourceTree = "";
95 | };
96 | /* End PBXGroup section */
97 |
98 | /* Begin PBXNativeTarget section */
99 | 73EB31AE25E5527000A9CAC1 /* Wandra */ = {
100 | isa = PBXNativeTarget;
101 | buildConfigurationList = 73EB31BF25E5527900A9CAC1 /* Build configuration list for PBXNativeTarget "Wandra" */;
102 | buildPhases = (
103 | 73EB31AB25E5527000A9CAC1 /* Sources */,
104 | 73EB31AC25E5527000A9CAC1 /* Frameworks */,
105 | 73EB31AD25E5527000A9CAC1 /* Resources */,
106 | );
107 | buildRules = (
108 | );
109 | dependencies = (
110 | );
111 | name = Wandra;
112 | productName = SNRChecker;
113 | productReference = 73EB31AF25E5527000A9CAC1 /* Wandra.app */;
114 | productType = "com.apple.product-type.application";
115 | };
116 | /* End PBXNativeTarget section */
117 |
118 | /* Begin PBXProject section */
119 | 73EB31A725E5527000A9CAC1 /* Project object */ = {
120 | isa = PBXProject;
121 | attributes = {
122 | BuildIndependentTargetsInParallel = YES;
123 | LastSwiftUpdateCheck = 1240;
124 | LastUpgradeCheck = 1500;
125 | TargetAttributes = {
126 | 73EB31AE25E5527000A9CAC1 = {
127 | CreatedOnToolsVersion = 12.4;
128 | };
129 | };
130 | };
131 | buildConfigurationList = 73EB31AA25E5527000A9CAC1 /* Build configuration list for PBXProject "Wandra" */;
132 | compatibilityVersion = "Xcode 9.3";
133 | developmentRegion = en;
134 | hasScannedForEncodings = 0;
135 | knownRegions = (
136 | en,
137 | Base,
138 | );
139 | mainGroup = 73EB31A625E5527000A9CAC1;
140 | productRefGroup = 73EB31B025E5527000A9CAC1 /* Products */;
141 | projectDirPath = "";
142 | projectRoot = "";
143 | targets = (
144 | 73EB31AE25E5527000A9CAC1 /* Wandra */,
145 | );
146 | };
147 | /* End PBXProject section */
148 |
149 | /* Begin PBXResourcesBuildPhase section */
150 | 73EB31AD25E5527000A9CAC1 /* Resources */ = {
151 | isa = PBXResourcesBuildPhase;
152 | buildActionMask = 2147483647;
153 | files = (
154 | 73EB31BA25E5527900A9CAC1 /* Preview Assets.xcassets in Resources */,
155 | 73F6F4BA262347CE005D666C /* sonar.mp3 in Resources */,
156 | 73EB31B725E5527900A9CAC1 /* Assets.xcassets in Resources */,
157 | );
158 | runOnlyForDeploymentPostprocessing = 0;
159 | };
160 | /* End PBXResourcesBuildPhase section */
161 |
162 | /* Begin PBXSourcesBuildPhase section */
163 | 73EB31AB25E5527000A9CAC1 /* Sources */ = {
164 | isa = PBXSourcesBuildPhase;
165 | buildActionMask = 2147483647;
166 | files = (
167 | 73EB31B525E5527000A9CAC1 /* ContentView.swift in Sources */,
168 | 73EB31B325E5527000A9CAC1 /* Main.swift in Sources */,
169 | 73EB31C425E6CAF500A9CAC1 /* Functions.swift in Sources */,
170 | 73B576662B0FBA3000460D0F /* Location.swift in Sources */,
171 | 738642B4261A4A580098091E /* HelpTexts.swift in Sources */,
172 | );
173 | runOnlyForDeploymentPostprocessing = 0;
174 | };
175 | /* End PBXSourcesBuildPhase section */
176 |
177 | /* Begin XCBuildConfiguration section */
178 | 73EB31BD25E5527900A9CAC1 /* Debug */ = {
179 | isa = XCBuildConfiguration;
180 | buildSettings = {
181 | ALWAYS_SEARCH_USER_PATHS = NO;
182 | CLANG_ANALYZER_NONNULL = YES;
183 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
184 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
185 | CLANG_CXX_LIBRARY = "libc++";
186 | CLANG_ENABLE_MODULES = YES;
187 | CLANG_ENABLE_OBJC_ARC = YES;
188 | CLANG_ENABLE_OBJC_WEAK = YES;
189 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
190 | CLANG_WARN_BOOL_CONVERSION = YES;
191 | CLANG_WARN_COMMA = YES;
192 | CLANG_WARN_CONSTANT_CONVERSION = YES;
193 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
194 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
195 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
196 | CLANG_WARN_EMPTY_BODY = YES;
197 | CLANG_WARN_ENUM_CONVERSION = YES;
198 | CLANG_WARN_INFINITE_RECURSION = YES;
199 | CLANG_WARN_INT_CONVERSION = YES;
200 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
201 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
202 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
203 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
204 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
205 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
206 | CLANG_WARN_STRICT_PROTOTYPES = YES;
207 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
208 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
209 | CLANG_WARN_UNREACHABLE_CODE = YES;
210 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
211 | COPY_PHASE_STRIP = NO;
212 | DEAD_CODE_STRIPPING = YES;
213 | DEBUG_INFORMATION_FORMAT = dwarf;
214 | ENABLE_STRICT_OBJC_MSGSEND = YES;
215 | ENABLE_TESTABILITY = YES;
216 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
217 | GCC_C_LANGUAGE_STANDARD = gnu11;
218 | GCC_DYNAMIC_NO_PIC = NO;
219 | GCC_NO_COMMON_BLOCKS = YES;
220 | GCC_OPTIMIZATION_LEVEL = 0;
221 | GCC_PREPROCESSOR_DEFINITIONS = (
222 | "DEBUG=1",
223 | "$(inherited)",
224 | );
225 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
226 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
227 | GCC_WARN_UNDECLARED_SELECTOR = YES;
228 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
229 | GCC_WARN_UNUSED_FUNCTION = YES;
230 | GCC_WARN_UNUSED_VARIABLE = YES;
231 | MACOSX_DEPLOYMENT_TARGET = 11.1;
232 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
233 | MTL_FAST_MATH = YES;
234 | ONLY_ACTIVE_ARCH = YES;
235 | SDKROOT = macosx;
236 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
237 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
238 | };
239 | name = Debug;
240 | };
241 | 73EB31BE25E5527900A9CAC1 /* Release */ = {
242 | isa = XCBuildConfiguration;
243 | buildSettings = {
244 | ALWAYS_SEARCH_USER_PATHS = NO;
245 | CLANG_ANALYZER_NONNULL = YES;
246 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
247 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
248 | CLANG_CXX_LIBRARY = "libc++";
249 | CLANG_ENABLE_MODULES = YES;
250 | CLANG_ENABLE_OBJC_ARC = YES;
251 | CLANG_ENABLE_OBJC_WEAK = YES;
252 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
253 | CLANG_WARN_BOOL_CONVERSION = YES;
254 | CLANG_WARN_COMMA = YES;
255 | CLANG_WARN_CONSTANT_CONVERSION = YES;
256 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
257 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
258 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
259 | CLANG_WARN_EMPTY_BODY = YES;
260 | CLANG_WARN_ENUM_CONVERSION = YES;
261 | CLANG_WARN_INFINITE_RECURSION = YES;
262 | CLANG_WARN_INT_CONVERSION = YES;
263 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
264 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
265 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
266 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
267 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
268 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
269 | CLANG_WARN_STRICT_PROTOTYPES = YES;
270 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
271 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
272 | CLANG_WARN_UNREACHABLE_CODE = YES;
273 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
274 | COPY_PHASE_STRIP = NO;
275 | DEAD_CODE_STRIPPING = YES;
276 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
277 | ENABLE_NS_ASSERTIONS = NO;
278 | ENABLE_STRICT_OBJC_MSGSEND = YES;
279 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
280 | GCC_C_LANGUAGE_STANDARD = gnu11;
281 | GCC_NO_COMMON_BLOCKS = YES;
282 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
283 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
284 | GCC_WARN_UNDECLARED_SELECTOR = YES;
285 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
286 | GCC_WARN_UNUSED_FUNCTION = YES;
287 | GCC_WARN_UNUSED_VARIABLE = YES;
288 | MACOSX_DEPLOYMENT_TARGET = 11.1;
289 | MTL_ENABLE_DEBUG_INFO = NO;
290 | MTL_FAST_MATH = YES;
291 | SDKROOT = macosx;
292 | SWIFT_COMPILATION_MODE = wholemodule;
293 | SWIFT_OPTIMIZATION_LEVEL = "-O";
294 | };
295 | name = Release;
296 | };
297 | 73EB31C025E5527900A9CAC1 /* Debug */ = {
298 | isa = XCBuildConfiguration;
299 | buildSettings = {
300 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
301 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
302 | CODE_SIGN_ENTITLEMENTS = Wandra/Wandra.entitlements;
303 | CODE_SIGN_IDENTITY = "Apple Development";
304 | CODE_SIGN_STYLE = Automatic;
305 | COMBINE_HIDPI_IMAGES = YES;
306 | DEAD_CODE_STRIPPING = YES;
307 | DEVELOPMENT_ASSET_PATHS = "\"Wandra/Preview Content\"";
308 | DEVELOPMENT_TEAM = F489D96499;
309 | ENABLE_HARDENED_RUNTIME = YES;
310 | ENABLE_PREVIEWS = YES;
311 | INFOPLIST_FILE = "$(SRCROOT)/Wandra/Info.plist";
312 | LD_RUNPATH_SEARCH_PATHS = (
313 | "$(inherited)",
314 | "@executable_path/../Frameworks",
315 | );
316 | MACOSX_DEPLOYMENT_TARGET = 11.0;
317 | MARKETING_VERSION = 1.2;
318 | PRODUCT_BUNDLE_IDENTIFIER = se.mikaellofgren.wandra;
319 | PRODUCT_NAME = "$(TARGET_NAME)";
320 | SWIFT_VERSION = 5.0;
321 | };
322 | name = Debug;
323 | };
324 | 73EB31C125E5527900A9CAC1 /* Release */ = {
325 | isa = XCBuildConfiguration;
326 | buildSettings = {
327 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
328 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
329 | CODE_SIGN_ENTITLEMENTS = Wandra/Wandra.entitlements;
330 | CODE_SIGN_IDENTITY = "Apple Development";
331 | CODE_SIGN_STYLE = Automatic;
332 | COMBINE_HIDPI_IMAGES = YES;
333 | DEAD_CODE_STRIPPING = YES;
334 | DEVELOPMENT_ASSET_PATHS = "\"Wandra/Preview Content\"";
335 | DEVELOPMENT_TEAM = F489D96499;
336 | ENABLE_HARDENED_RUNTIME = YES;
337 | ENABLE_PREVIEWS = YES;
338 | INFOPLIST_FILE = "$(SRCROOT)/Wandra/Info.plist";
339 | LD_RUNPATH_SEARCH_PATHS = (
340 | "$(inherited)",
341 | "@executable_path/../Frameworks",
342 | );
343 | MACOSX_DEPLOYMENT_TARGET = 11.0;
344 | MARKETING_VERSION = 1.2;
345 | PRODUCT_BUNDLE_IDENTIFIER = se.mikaellofgren.wandra;
346 | PRODUCT_NAME = "$(TARGET_NAME)";
347 | SWIFT_VERSION = 5.0;
348 | };
349 | name = Release;
350 | };
351 | /* End XCBuildConfiguration section */
352 |
353 | /* Begin XCConfigurationList section */
354 | 73EB31AA25E5527000A9CAC1 /* Build configuration list for PBXProject "Wandra" */ = {
355 | isa = XCConfigurationList;
356 | buildConfigurations = (
357 | 73EB31BD25E5527900A9CAC1 /* Debug */,
358 | 73EB31BE25E5527900A9CAC1 /* Release */,
359 | );
360 | defaultConfigurationIsVisible = 0;
361 | defaultConfigurationName = Release;
362 | };
363 | 73EB31BF25E5527900A9CAC1 /* Build configuration list for PBXNativeTarget "Wandra" */ = {
364 | isa = XCConfigurationList;
365 | buildConfigurations = (
366 | 73EB31C025E5527900A9CAC1 /* Debug */,
367 | 73EB31C125E5527900A9CAC1 /* Release */,
368 | );
369 | defaultConfigurationIsVisible = 0;
370 | defaultConfigurationName = Release;
371 | };
372 | /* End XCConfigurationList section */
373 | };
374 | rootObject = 73EB31A725E5527000A9CAC1 /* Project object */;
375 | }
376 |
--------------------------------------------------------------------------------
/Wandra.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Wandra.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Wandra.xcodeproj/project.xcworkspace/xcuserdata/mikael.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mikaellofgren/wandra/15e1ebfc1d3c2784c29bb441a68a4901564ab5d4/Wandra.xcodeproj/project.xcworkspace/xcuserdata/mikael.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/Wandra.xcodeproj/xcshareddata/xcschemes/Wandra.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
44 |
46 |
52 |
53 |
54 |
55 |
61 |
63 |
69 |
70 |
71 |
72 |
74 |
75 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/Wandra.xcodeproj/xcuserdata/mikael.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
--------------------------------------------------------------------------------
/Wandra.xcodeproj/xcuserdata/mikael.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | SNRChecker.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 | Wandra.xcscheme_^#shared#^_
13 |
14 | orderHint
15 | 0
16 |
17 |
18 | SuppressBuildableAutocreation
19 |
20 | 73EB31AE25E5527000A9CAC1
21 |
22 | primary
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/Wandra/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "wandra-ikon-16.png",
5 | "idiom" : "mac",
6 | "scale" : "1x",
7 | "size" : "16x16"
8 | },
9 | {
10 | "filename" : "wandra-ikon-32.png",
11 | "idiom" : "mac",
12 | "scale" : "2x",
13 | "size" : "16x16"
14 | },
15 | {
16 | "filename" : "wandra-ikon-33.png",
17 | "idiom" : "mac",
18 | "scale" : "1x",
19 | "size" : "32x32"
20 | },
21 | {
22 | "filename" : "wandra-ikon-64.png",
23 | "idiom" : "mac",
24 | "scale" : "2x",
25 | "size" : "32x32"
26 | },
27 | {
28 | "filename" : "wandra-ikon-128.png",
29 | "idiom" : "mac",
30 | "scale" : "1x",
31 | "size" : "128x128"
32 | },
33 | {
34 | "filename" : "wandra-ikon-256.png",
35 | "idiom" : "mac",
36 | "scale" : "2x",
37 | "size" : "128x128"
38 | },
39 | {
40 | "filename" : "wandra-ikon-257.png",
41 | "idiom" : "mac",
42 | "scale" : "1x",
43 | "size" : "256x256"
44 | },
45 | {
46 | "filename" : "wandra-ikon-512.png",
47 | "idiom" : "mac",
48 | "scale" : "2x",
49 | "size" : "256x256"
50 | },
51 | {
52 | "filename" : "wandra-ikon-513.png",
53 | "idiom" : "mac",
54 | "scale" : "1x",
55 | "size" : "512x512"
56 | },
57 | {
58 | "filename" : "wandra-ikon-1024.png",
59 | "idiom" : "mac",
60 | "scale" : "2x",
61 | "size" : "512x512"
62 | }
63 | ],
64 | "info" : {
65 | "author" : "xcode",
66 | "version" : 1
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/Wandra/Assets.xcassets/AppIcon.appiconset/wandra-ikon-1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mikaellofgren/wandra/15e1ebfc1d3c2784c29bb441a68a4901564ab5d4/Wandra/Assets.xcassets/AppIcon.appiconset/wandra-ikon-1024.png
--------------------------------------------------------------------------------
/Wandra/Assets.xcassets/AppIcon.appiconset/wandra-ikon-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mikaellofgren/wandra/15e1ebfc1d3c2784c29bb441a68a4901564ab5d4/Wandra/Assets.xcassets/AppIcon.appiconset/wandra-ikon-128.png
--------------------------------------------------------------------------------
/Wandra/Assets.xcassets/AppIcon.appiconset/wandra-ikon-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mikaellofgren/wandra/15e1ebfc1d3c2784c29bb441a68a4901564ab5d4/Wandra/Assets.xcassets/AppIcon.appiconset/wandra-ikon-16.png
--------------------------------------------------------------------------------
/Wandra/Assets.xcassets/AppIcon.appiconset/wandra-ikon-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mikaellofgren/wandra/15e1ebfc1d3c2784c29bb441a68a4901564ab5d4/Wandra/Assets.xcassets/AppIcon.appiconset/wandra-ikon-256.png
--------------------------------------------------------------------------------
/Wandra/Assets.xcassets/AppIcon.appiconset/wandra-ikon-257.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mikaellofgren/wandra/15e1ebfc1d3c2784c29bb441a68a4901564ab5d4/Wandra/Assets.xcassets/AppIcon.appiconset/wandra-ikon-257.png
--------------------------------------------------------------------------------
/Wandra/Assets.xcassets/AppIcon.appiconset/wandra-ikon-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mikaellofgren/wandra/15e1ebfc1d3c2784c29bb441a68a4901564ab5d4/Wandra/Assets.xcassets/AppIcon.appiconset/wandra-ikon-32.png
--------------------------------------------------------------------------------
/Wandra/Assets.xcassets/AppIcon.appiconset/wandra-ikon-33.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mikaellofgren/wandra/15e1ebfc1d3c2784c29bb441a68a4901564ab5d4/Wandra/Assets.xcassets/AppIcon.appiconset/wandra-ikon-33.png
--------------------------------------------------------------------------------
/Wandra/Assets.xcassets/AppIcon.appiconset/wandra-ikon-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mikaellofgren/wandra/15e1ebfc1d3c2784c29bb441a68a4901564ab5d4/Wandra/Assets.xcassets/AppIcon.appiconset/wandra-ikon-512.png
--------------------------------------------------------------------------------
/Wandra/Assets.xcassets/AppIcon.appiconset/wandra-ikon-513.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mikaellofgren/wandra/15e1ebfc1d3c2784c29bb441a68a4901564ab5d4/Wandra/Assets.xcassets/AppIcon.appiconset/wandra-ikon-513.png
--------------------------------------------------------------------------------
/Wandra/Assets.xcassets/AppIcon.appiconset/wandra-ikon-64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mikaellofgren/wandra/15e1ebfc1d3c2784c29bb441a68a4901564ab5d4/Wandra/Assets.xcassets/AppIcon.appiconset/wandra-ikon-64.png
--------------------------------------------------------------------------------
/Wandra/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Wandra/Assets.xcassets/DarkGreen.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0.000",
9 | "green" : "0.560",
10 | "red" : "0.000"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0.000",
27 | "green" : "0.560",
28 | "red" : "0.000"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Wandra/Assets.xcassets/LightGreen.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0.000",
9 | "green" : "0.979",
10 | "red" : "0.556"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0.000",
27 | "green" : "0.979",
28 | "red" : "0.556"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Wandra/Assets.xcassets/Orange.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0.000",
9 | "green" : "0.578",
10 | "red" : "1.000"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0.000",
27 | "green" : "0.578",
28 | "red" : "1.000"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Wandra/Assets.xcassets/Red.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0.000",
9 | "green" : "0.149",
10 | "red" : "1.000"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0.000",
27 | "green" : "0.149",
28 | "red" : "1.000"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Wandra/Assets.xcassets/Yellow.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0.000",
9 | "green" : "0.986",
10 | "red" : "0.999"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0.000",
27 | "green" : "0.986",
28 | "red" : "0.999"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Wandra/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // Wandra
4 | //
5 | // Created by Mikael Löfgren on 2021-02-23.
6 | //
7 |
8 | import SwiftUI
9 | import AVKit
10 | import CoreLocation
11 |
12 | // basestationsArray contains only unique basestations ID, is used to get all custom names from basestationsWithNameArray
13 | var basestationsArray = Set()
14 | var basestationsWithNameArray = [String: String]()
15 | var ssidArray = [SSID]()
16 | var timeInterval: Double = 2
17 | var baseStationIDFirstTime = true
18 | var rssiFirstTime = true
19 | var locationWarningHasBeenShown = 0
20 |
21 | struct SSID {
22 | var ssid: String
23 | var bssid: String
24 | var apname: String
25 | var snr: Int
26 | var strength: Int
27 | var time: String
28 | }
29 |
30 | // 1.2 added CustomUserLocationDelegate and func userLocationUpdated below and LocationController and Vstack .onAppear func
31 | struct ContentView: View, CustomUserLocationDelegate {
32 | func userLocationUpdated(location: CLLocation) {
33 | // print("Location Updated")
34 | }
35 | @Environment(\.colorScheme) var colorScheme
36 | @State private var ssidName: String = ""
37 | @State private var snrValue: Float = 0
38 | @State private var snr: Int = 0
39 | @State private var snrText: String = ""
40 | @State private var rssiValue: Float = 0
41 | @State private var strength: Int = 0
42 | @State private var strengthText: String = ""
43 | @State private var basestationID: String = ""
44 | @State private var baseStationTextFieldName: String = ""
45 | @State private var stepperValue = timeInterval
46 | @State private var timer = Timer.publish(every: TimeInterval(timeInterval), on: .main, in: .common).autoconnect()
47 | @State private var buttonPressed: Bool = true
48 | @State private var buttonImage = "pause.fill"
49 | @State private var soundImage = "speaker.slash.fill"
50 | @State private var soundMuted: Bool = true
51 |
52 |
53 |
54 | var body: some View {
55 | ZStack(alignment: .top) {
56 | Spacer()
57 | .background(colorScheme == .dark ? Color.black : Color.white)
58 | .edgesIgnoringSafeArea(.all)
59 | .frame(width: 250, height: 715)
60 |
61 | VStack {
62 | SSIDView(ssidname: self.$ssidName)
63 | SNRView(signalToNoise: self.$snrValue, snr: self.$snr, snrText: self.$snrText )
64 | RSSIView(rssi: self.$rssiValue, strength: self.$strength, strengthText: self.$strengthText, isSoundMuted: self.$soundMuted )
65 | BasestationView(baseStationID: self.$basestationID)
66 | BasestationTextFieldView(baseStationName: self.$baseStationTextFieldName, baseStationID: self.$basestationID)
67 | .onReceive(timer) { _ in
68 | withAnimation {
69 | self.runApp()
70 | }
71 | }
72 | .onAppear(perform: {
73 | if LocationServices.shared.locationManager.authorizationStatus == .authorizedAlways {
74 | LocationServices.shared.userLocationDelegate = self
75 | } else {
76 | LocationServices.shared.locationManager.requestAlwaysAuthorization()
77 | }
78 | })
79 | Divider().frame(width: 200).padding(.bottom, 5)
80 | Button(action: {
81 | self.buttonStartStop()
82 | }){
83 | Image(systemName: self.buttonImage)
84 | .font(.largeTitle)
85 | .frame(width: 50, height:50)
86 | .foregroundColor(Color.white)
87 | .background(Color("DarkGreen"))
88 | .clipShape(Circle())
89 | }.buttonStyle(PlainButtonStyle())
90 | .padding(.bottom, 5)
91 | .help(buttonHelp)
92 | .contextMenu {
93 | Button(action: {
94 | // Get every items from array and add in to csvString
95 | var csvString = "ssid, bssid, apname, snr, strength, time\n"
96 | for every in ssidArray {
97 | csvString += ("\(every.ssid), \(every.bssid), \(every.apname), \(every.snr), \(every.strength), \(every.time)\n")
98 | }
99 |
100 | // Add the custom basestations name to the whole csv string, otherwise it will output only right name from when name was entered to Array, using wildcard for matching
101 | for basestations in basestationsArray {
102 | csvString = csvString.replacingOccurrences(of: "\(basestations)(,)(.*?)(,)", with: "\(basestations), \(basestationsWithNameArray[basestations] ?? ""),",
103 | options: .regularExpression)
104 | }
105 | saveMeasuredData(csvString)
106 | }) {
107 | Text("Save as .csv file")
108 | }
109 | }
110 |
111 | Stepper("Measure interval: \(stepperValue, specifier: "%g") in seconds",
112 | onIncrement: {
113 | stepperValue += 1
114 | if stepperValue >= 10 { stepperValue = 10 }
115 | timeInterval = stepperValue
116 | self.stopTimer()
117 | self.startTimer()
118 | }, onDecrement: {
119 | stepperValue -= 1
120 | if stepperValue <= 1 { stepperValue = 1 }
121 | timeInterval = stepperValue
122 | self.stopTimer()
123 | self.startTimer()
124 | })
125 |
126 | Button(action: {
127 | soundButtonStartStop ()
128 | }){
129 | Image(systemName: self.soundImage)
130 | }.help(soundHelp)
131 | }
132 | }
133 | }
134 |
135 | struct SSIDView: View {
136 | @Binding var ssidname: String
137 | var body: some View {
138 | VStack {
139 | Text("\(ssidname)")
140 | .frame(width: 165, height: 20)
141 | .background(RoundedRectangle(cornerRadius: 20.0).fill(Color.gray).opacity(0.3))
142 | .help(ssidHelp)
143 | }.padding(.bottom, 5)
144 | }
145 | }
146 |
147 | struct SNRView: View {
148 | @Binding var signalToNoise: Float
149 | @Binding var snr: Int
150 | @Binding var snrText: String
151 | @State private var showHelpPopover = false
152 | var body: some View {
153 | VStack {
154 | Divider().frame(width: 200)
155 | Text("Signal to Noise Ratio")
156 | .onTapGesture {
157 | showHelpPopover = true
158 | }
159 | .popover(isPresented: $showHelpPopover, arrowEdge: .leading) {
160 | ZStack {
161 | Color("Yellow")
162 | .scaleEffect(5)
163 | Text(snrHelpPopover)
164 | .foregroundColor(Color.black)
165 | .padding(8)
166 | }
167 | }
168 | .help(snrHelp)
169 | .padding(.bottom, 20)
170 |
171 | ZStack {
172 | Circle()
173 | .stroke(lineWidth: 20.0)
174 | .opacity(0.3)
175 | .foregroundColor(Color.gray)
176 |
177 | Circle()
178 | .trim(from: 0.0, to: CGFloat(min(self.signalToNoise, 1.0)))
179 | .stroke(style: StrokeStyle(lineWidth: 20.0, lineCap: .round, lineJoin: .round))
180 | .foregroundColor(getSignalToNoiseRatio().snrColor)
181 | .rotationEffect(Angle(degrees: 270.0))
182 | .animation(.linear)
183 | .help(snrValueHelp)
184 |
185 | Text("\(self.snr)")
186 | .font(.largeTitle)
187 | .bold()
188 | .padding(.bottom, 20)
189 |
190 | Text("\(self.snrText)")
191 | .padding(.top, 20)
192 | .help(snrValueHelp)
193 | } .frame(width: 150.0, height: 150.0)
194 | .padding(.bottom, 20)
195 | }
196 | }
197 | }
198 |
199 | struct RSSIView: View {
200 | @Binding var rssi: Float
201 | @Binding var strength: Int
202 | @Binding var strengthText: String
203 | @Binding var isSoundMuted: Bool
204 | @State var audioPlayer: AVAudioPlayer!
205 | @State var showHelpPopover = false
206 | var body: some View {
207 | ZStack {
208 | // Used only to get right space as BaseStation View
209 | }
210 | VStack {
211 | Divider().frame(width: 200)
212 | Text("Received Signal Strength Indicator")
213 | .onTapGesture {
214 | showHelpPopover = true
215 | }
216 | .popover(isPresented: $showHelpPopover, arrowEdge: .leading) {
217 | ZStack {
218 | Color("Yellow")
219 | .scaleEffect(5)
220 | Text(rssiHelpPopover)
221 | .foregroundColor(Color.black)
222 | .padding(8)
223 | }
224 | }
225 | .help(rssiHelp)
226 | .padding(.bottom, 20)
227 |
228 | ZStack {
229 | Circle()
230 | .stroke(lineWidth: 20.0)
231 | .opacity(0.3)
232 | .foregroundColor(Color.gray)
233 |
234 | Circle()
235 | .trim(from: 0.0, to: CGFloat(min(self.rssi, 1.0)))
236 | .stroke(style: StrokeStyle(lineWidth: 20.0, lineCap: .round, lineJoin: .round))
237 | .foregroundColor(getStrength().strengthColor)
238 | .rotationEffect(Angle(degrees: 270.0))
239 | .animation(.linear)
240 | .help(rssiValueHelp)
241 | Text("-\(self.strength)")
242 | .font(.largeTitle)
243 | .bold()
244 | .padding(.bottom, 20)
245 | .onChange(of: strength) {[strength] newValue in
246 | if rssiFirstTime == false {
247 | let strengthHigherThenLast = newValue-3
248 | if strength < strengthHigherThenLast && isSoundMuted == false {
249 | let sound = Bundle.main.path(forResource: "sonar", ofType: "mp3")
250 | self.audioPlayer = try! AVAudioPlayer(contentsOf: URL(fileURLWithPath: sound!))
251 | self.audioPlayer.play()
252 | }
253 | } else { rssiFirstTime = false }
254 | }
255 | Text("\(self.strengthText)")
256 | .padding(.top, 20)
257 | .help(rssiValueHelp)
258 | } .frame(width: 150.0, height: 150.0)
259 | .padding(.bottom, 20)
260 | }
261 | }
262 | }
263 |
264 | struct BasestationView: View {
265 | @Binding var baseStationID: String
266 | @State private var showingPopover = false
267 | @State private var showHelpPopover = false
268 | var body: some View {
269 |
270 | ZStack {
271 | // Used only for popover, couldnt have two popovers in same stack
272 | }
273 | .onChange(of: baseStationID) { newValue in
274 | // Dont show popover first time starting app or no ssid, and show only when we got a bssid
275 | if baseStationIDFirstTime == true {
276 | showingPopover = false
277 | if baseStationID != "" {
278 | baseStationIDFirstTime = false
279 | }
280 | } else {
281 | showingPopover = true
282 | }
283 | }
284 | .popover(isPresented: $showingPopover, arrowEdge: .leading) {
285 | ZStack {
286 | Color("DarkGreen")
287 | .scaleEffect(5)
288 | Text("Roaming...")
289 | .foregroundColor(Color.white)
290 | .padding(8)
291 | }
292 | }.offset(x: -82, y: 32)
293 |
294 | VStack {
295 | Divider().frame(width: 200)
296 | .padding(.bottom, 5)
297 | Text("\(baseStationID)")
298 | .padding()
299 | .frame(width: 165, height: 20)
300 | .background(RoundedRectangle(cornerRadius: 20.0).fill(Color.gray).opacity(0.3))
301 | .help(bssidHelp)
302 | .onTapGesture {
303 | showHelpPopover = true
304 | }
305 | .popover(isPresented: $showHelpPopover, arrowEdge: .leading) {
306 | ZStack {
307 | Color("Yellow")
308 | .scaleEffect(5)
309 | Text(bssidHelpPopover)
310 | .foregroundColor(Color.black)
311 | .padding(8)
312 | }
313 | }
314 | .contextMenu {
315 | Button(action: {
316 | let pasteboard = NSPasteboard.general
317 | pasteboard.declareTypes([.string], owner: nil)
318 | pasteboard.setString("\(baseStationID)", forType: .string)
319 | }) {
320 | Text("Copy")
321 | }
322 | }
323 | }
324 | }
325 | }
326 |
327 | struct BasestationTextFieldView: View {
328 | @Binding var baseStationName: String
329 | @Binding var baseStationID: String
330 |
331 | var body: some View {
332 | VStack {
333 | // Set name to matching BaseStationID, disable textfield if BaseStationID is empty
334 | if baseStationID != "" {
335 | TextField("Access point name", text: $baseStationName, onCommit: {
336 | basestationsWithNameArray["\(getBSSIDName())"] = "\(baseStationName)"
337 | })
338 | .frame(width: 165, height: 20)
339 | .multilineTextAlignment(.center)
340 | .textFieldStyle(RoundedBorderTextFieldStyle())
341 | .help(apNameHelp)
342 | .onChange(of: baseStationID) { newValue in
343 | // Show custom ap name from matching BaseStationID
344 | if let getMatchingName = basestationsWithNameArray[baseStationID] {
345 | baseStationName = getMatchingName
346 | NSApp.keyWindow?.makeFirstResponder(nil)
347 | } else {
348 | baseStationName = ""
349 | }
350 | }
351 | } else {
352 | TextField("Access point name", text: $baseStationID).disabled(/*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/)
353 | .frame(width: 165, height: 20)
354 | .multilineTextAlignment(.center)
355 | .textFieldStyle(RoundedBorderTextFieldStyle())
356 | .help(apNameDisabledHelp)
357 | }
358 | }.padding(.bottom, 5)
359 |
360 | }
361 | }
362 |
363 |
364 | func stopTimer() {
365 | self.timer.upstream.connect().cancel()
366 | }
367 |
368 | func startTimer() {
369 | self.timer = Timer.publish(every: TimeInterval(timeInterval), on: .main, in: .common).autoconnect()
370 | }
371 |
372 | func buttonStartStop() {
373 | if self.buttonPressed == true {
374 | self.stopTimer()
375 | self.resetValues()
376 | self.buttonPressed = false
377 | self.buttonImage = "play.fill"
378 | } else {
379 | self.buttonPressed = false
380 | self.startTimer()
381 | self.buttonPressed = true
382 | self.buttonImage = "pause.fill"
383 | }
384 | }
385 |
386 | func soundButtonStartStop () {
387 | if self.soundMuted == true {
388 | self.soundImage = "speaker.3.fill"
389 | self.soundMuted = false
390 | } else {
391 | self.soundImage = "speaker.slash.fill"
392 | self.soundMuted = true
393 | }
394 | }
395 |
396 | func resetValues() {
397 | // reset all fields
398 | self.ssidName = ""
399 | self.snrValue = 0
400 | self.snr = 0
401 | self.snrText = ""
402 | self.rssiValue = 0
403 | self.strength = 0
404 | self.strengthText = ""
405 | self.basestationID = ""
406 | self.buttonPressed = true
407 | baseStationIDFirstTime = true
408 | }
409 |
410 | func runApp() {
411 | hideWindowButtons()
412 |
413 | if getSSIDName() == "" {
414 | self.buttonStartStop()
415 | // Show alert
416 | let info = NSAlert()
417 | info.icon = NSImage (named: NSImage.cautionName)
418 | info.addButton(withTitle: "OK")
419 | info.alertStyle = NSAlert.Style.informational
420 | info.messageText = "Wi-Fi or Location seems disabled"
421 | info.informativeText = "Enable Wi-Fi and also check System Settings - Privacy & Security - Location Services and enable for Wandra, then press play button to try again."
422 | info.runModal()
423 |
424 | return
425 | }
426 | self.ssidName = getSSIDName()
427 | self.snrValue = Float(getSignalToNoiseRatio().snr)/100*2 // Better matching with scale of 50
428 | self.snr = getSignalToNoiseRatio().snr
429 | self.snrText = getSignalToNoiseRatio().snrText
430 | self.rssiValue = Float(getStrength().strength)/100 // Better matching with scale of 100
431 | self.strength = getStrength().strength
432 | self.strengthText = getStrength().strengthText
433 | self.basestationID = getBSSIDName()
434 | basestationsArray.insert(self.basestationID)
435 |
436 | // Get the baseStationTextFieldName, so its output correct name to ssidArray
437 | if basestationsWithNameArray.isEmpty {
438 | self.baseStationTextFieldName = ""
439 | } else {
440 | if let name = basestationsWithNameArray[getBSSIDName()] {
441 | self.baseStationTextFieldName = name
442 | } else {
443 | self.baseStationTextFieldName = ""
444 | }
445 | }
446 |
447 | // Append all data to ssidArray
448 | ssidArray.append(contentsOf:[SSID(ssid: self.ssidName, bssid: self.basestationID, apname: self.baseStationTextFieldName, snr: self.snr, strength: self.strength, time: timeNowString ())])
449 | }
450 | }
451 |
452 |
453 |
454 |
--------------------------------------------------------------------------------
/Wandra/Functions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Functions.swift
3 | // Wandra
4 | //
5 | // Created by Mikael Löfgren on 2021-02-24.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 | import CoreWLAN
11 | import CoreLocation
12 |
13 | func hideWindowButtons() {
14 | // Hide window buttons and remove tabbing and fullscreen
15 | for window in NSApplication.shared.windows {
16 | window.standardWindowButton(NSWindow.ButtonType.zoomButton)?.isHidden = true
17 | window.standardWindowButton(NSWindow.ButtonType.miniaturizeButton)?.isHidden = true
18 | window.standardWindowButton(NSWindow.ButtonType.closeButton)?.isHidden = true
19 | window.backgroundColor = NSColor.white
20 | window.collectionBehavior = .fullScreenNone
21 | NSWindow.allowsAutomaticWindowTabbing = false
22 | }
23 | }
24 |
25 | func getSSIDName () -> String {
26 | let ssid = CWWiFiClient.shared().interface(withName: nil)?.ssid() ?? ""
27 | return ssid
28 | }
29 |
30 | func getSignalToNoiseRatio () -> (snr: Int, snrText: String, snrColor: Color){
31 | var noise = CWWiFiClient.shared().interface()?.noiseMeasurement() ?? 0
32 | noise = abs(noise)
33 | let snr = noise-getStrength().strength
34 | var snrText = ""
35 | var snrColor = Color("LightGreen")
36 |
37 | switch snr {
38 | case 0...9 :
39 | snrText = "Very poor signal"
40 | snrColor = Color("Red")
41 | case 10...15 :
42 | snrText = "Very low signal"
43 | snrColor = Color("Orange")
44 | case 16...25 :
45 | snrText = "Low signal"
46 | snrColor = Color("Yellow")
47 | case 26...40 :
48 | snrText = "Very good signal"
49 | snrColor = Color("LightGreen")
50 | case 41...100 :
51 | snrText = "Excellent signal"
52 | snrColor = Color("DarkGreen")
53 | default :
54 | print("Cant scalculate snr signal")
55 | }
56 |
57 | return (snr, snrText, snrColor)
58 | }
59 |
60 | func getStrength () -> (strength: Int, strengthText: String, strengthColor: Color) {
61 | var strength = CWWiFiClient.shared().interface()?.rssiValue() ?? 0
62 | strength = abs(strength)
63 | var strengthText = ""
64 | var strengthColor = Color("LightGreen")
65 |
66 | switch strength {
67 | case 0...49 :
68 | strengthText = "Maximum signal"
69 | strengthColor = Color("DarkGreen")
70 | case 50...59 :
71 | strengthText = "Excellent signal"
72 | strengthColor = Color("DarkGreen")
73 | case 60...67 :
74 | strengthText = "Very good signal"
75 | strengthColor = Color("LightGreen")
76 | case 68...70 :
77 | strengthText = "Low signal"
78 | strengthColor = Color("Yellow")
79 | case 71...79 :
80 | strengthText = "Very low signal"
81 | strengthColor = Color("Orange")
82 | case 80...100 :
83 | strengthText = "Very poor signal"
84 | strengthColor = Color("Red")
85 | default :
86 | print("Cant calculate strength signal")
87 | }
88 | return (strength, strengthText, strengthColor)
89 | }
90 |
91 | func normalise(macString: String) -> String {
92 | // https://developer.apple.com/forums/thread/50302
93 | return String(macString
94 | .uppercased()
95 | .split(separator: ":")
96 | .map{("00" + $0).suffix(2)}
97 | .joined(separator: ":")
98 | )
99 | }
100 |
101 | func getBSSIDName () -> String {
102 | var bssid = CWWiFiClient.shared().interface()?.bssid() ?? ""
103 | bssid = normalise(macString: bssid)
104 | return bssid
105 | }
106 |
107 | func timeNowString () -> String {
108 | let formatter = DateFormatter()
109 | formatter.timeStyle = .medium
110 | return formatter.string(from: Date())
111 | }
112 |
113 | func dateNowString () -> String {
114 | let formatter = DateFormatter()
115 | formatter.dateStyle = .short
116 | return formatter.string(from: Date())
117 | }
118 |
119 | func saveMeasuredData (_ stringToBeSaved: String) {
120 | // Save Dialog
121 | let dialog = NSSavePanel();
122 | dialog.showsResizeIndicator = true;
123 | dialog.showsHiddenFiles = false;
124 | dialog.canCreateDirectories = true;
125 | // Default Save value, add .csv
126 | dialog.nameFieldStringValue = "measured_data_\(dateNowString()).csv"
127 |
128 |
129 | if (dialog.runModal() == NSApplication.ModalResponse.OK) {
130 | let result = dialog.url // Pathname of the file
131 | if (result != nil) {
132 | let path = result!.path
133 |
134 | let documentDirURL = URL(fileURLWithPath: path)
135 | // Save data to file
136 | let fileURL = documentDirURL
137 | let writeString = stringToBeSaved
138 | do {
139 | // Write to the file
140 | try writeString.write(to: fileURL, atomically: true, encoding: String.Encoding.utf8)
141 | } catch let error as NSError {
142 | print("Failed writing to URL: \(fileURL), Error: " + error.localizedDescription)
143 | let info = NSAlert()
144 | info.icon = NSImage (named: NSImage.cautionName)
145 | info.addButton(withTitle: "OK")
146 | info.alertStyle = NSAlert.Style.informational
147 | info.messageText = "Something went wrong when saving file to"
148 | info.informativeText = "\(path)"
149 | info.runModal()
150 | }
151 |
152 | let info = NSAlert()
153 | info.icon = NSImage(systemSymbolName: "doc.plaintext", accessibilityDescription: nil)
154 | info.addButton(withTitle: "OK")
155 | info.alertStyle = NSAlert.Style.informational
156 | info.messageText = "Successfully saved file to"
157 | info.informativeText = "\(path)"
158 | info.runModal()
159 | }
160 | } else {
161 | // User clicked on "Cancel"
162 | return
163 | }
164 | // End Save Dialog
165 | }
166 |
167 | extension NSTextField {
168 | // Workaround to remove textfield focus ring around textfield
169 | open override var focusRingType: NSFocusRingType {
170 | get { .none }
171 | set { }
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/Wandra/HelpTexts.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HelpTexts.swift
3 | // Wandra
4 | //
5 | // Created by Mikael Löfgren on 2021-04-04.
6 | //
7 |
8 | import Foundation
9 |
10 | let ssidHelp = ("""
11 | Service set identifier (SSID)
12 | Name of current connected Wi-Fi network
13 | """)
14 |
15 | let snrHelp = ("""
16 | Signal to Noise Ratio (SNR)
17 | Higher value is better
18 |
19 | Click to show more info
20 | """)
21 |
22 | let snrHelpPopover = ("""
23 | Signal to Noise Ratio (SNR)
24 | Higher value is better
25 |
26 | Its measured by taking the signal strength (RSSI)
27 | and subtracting the noise.
28 | Noise is all the RF around the radio in the spectrum
29 | from all RF sources. It can come from a microwave,
30 | bluetooth devices, etc.
31 |
32 | Wireless adapters (NIC) for laptops, tablets,
33 | aren’t capable of determining accurate SNR.
34 | An example is when a microwave is turned on.
35 | The NIC will not be able to see the RF signal
36 | because the microwave is sending unmodulated bits.
37 | Thus the built-in NIC believes there is no noise.
38 | """)
39 |
40 |
41 |
42 | let snrValueHelp = ("""
43 | Full circle is 50
44 | 0-9 = Very poor signal
45 | 10-15 = Very low signal
46 | 16-25 = Low signal
47 | 26-40 = Very good signal
48 | 41-99 = Excellent signal
49 | """)
50 |
51 | let rssiHelp = ("""
52 | Received Signal Strength Indicator (RSSI)
53 | Lower value is better
54 |
55 | Click to show more info
56 | """)
57 |
58 | let rssiHelpPopover = ("""
59 | Received Signal Strength Indicator (RSSI)
60 | Measured in dBm (decibel milliwatts)
61 |
62 | Lower value is better
63 | Every 3 dB lower = doubles signal strength
64 | Every 3 dB higher = halves signal strength
65 | Usually it starts around -30 near the access point,
66 | and gets higher (losing strength)
67 | when moving away from the access point.
68 | """)
69 |
70 | let rssiValueHelp = ("""
71 | Full circle is -100 (negative scale)
72 | 0-49 = Maximum signal
73 | 50-59 = Excellent signal
74 | 60-67 = Very good signal
75 | 68-70 = Low signal
76 | 71-79 = Very low signal
77 | 80-100 = Very poor signal
78 | """)
79 |
80 | let bssidHelp = ("""
81 | Basic Service Set Identifiers (BSSID)
82 | Right click to copy the value
83 |
84 | Click to show more info
85 | """)
86 |
87 | let bssidHelpPopover = ("""
88 | Basic Service Set Identifiers (BSSID)
89 | Most of the time it is associated with MAC address of the access point
90 | Stick to current BSSID until:
91 | - RSSI below -75dBm
92 | - Choose 5Ghz over 2.4GHz as long as 5GHz is more than -68dBm
93 | - Does NOT support 802.11k
94 | - Choose AP with RSSI 12dB higher than current AP
95 |
96 | If multiple 5 GHz SSIDs meet this level,
97 | macOS chooses a network based on these criteria:
98 | - 802.11ax is preferred over 802.11ac.
99 | - 802.11ac is preferred over 802.11n or 802.11a.
100 | - 802.11n is preferred over 802.11a.
101 | - 80 MHz channel width is preferred over 40 MHz or 20 MHz.
102 | - 40 MHz channel width is preferred over 20 MHz.
103 |
104 | https://support.apple.com/en-us/HT206207
105 | """)
106 |
107 | let apNameHelp = ("""
108 | Custom access point name (example Floor 2)
109 | Textfield reloads every measure interval,
110 | set higher interval to easier type a value,
111 | then press enter to save
112 | """)
113 |
114 | let apNameDisabledHelp = "Disabled until a BSSID is found"
115 |
116 | let buttonHelp = "Right click to save measured data as a csv file"
117 |
118 | let soundHelp = """
119 | Plays a sound if RSSI has a value higher than three (losing strength),
120 | compared to the last measured value
121 | """
122 |
--------------------------------------------------------------------------------
/Wandra/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | $(MARKETING_VERSION)
19 | CFBundleVersion
20 | 1
21 | LSApplicationCategoryType
22 | public.app-category.utilities
23 | LSMinimumSystemVersion
24 | $(MACOSX_DEPLOYMENT_TARGET)
25 |
26 |
27 |
--------------------------------------------------------------------------------
/Wandra/Location.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Location.swift
3 | // Wandra
4 | //
5 | // Borrowed from
6 | // https://github.com/ehemmete/NetworkView/blob/main/NetworkView/Models/NetworkWorkflow.swift
7 |
8 | // Signing and Capabilities - App Sandbox Location is needed for BSSID and Outgoing connections client and import CoreLocation
9 |
10 | import Foundation
11 | import CoreLocation
12 | import Network
13 | import SwiftUI
14 |
15 | protocol CustomUserLocationDelegate {
16 | func userLocationUpdated(location: CLLocation)
17 | }
18 | class LocationServices: NSObject, CLLocationManagerDelegate, ObservableObject {
19 | @State var presentMainAlert = false
20 | public static let shared = LocationServices()
21 | var userLocationDelegate: CustomUserLocationDelegate?
22 | let locationManager = CLLocationManager()
23 |
24 |
25 | private override init() {
26 | super.init()
27 | locationManager.delegate = self
28 | }
29 |
30 | func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
31 | print("Location Changed")
32 | }
33 |
34 | func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
35 | print("Authorization Changed")
36 | presentMainAlert = true
37 | }
38 |
39 | }
40 |
41 |
--------------------------------------------------------------------------------
/Wandra/Main.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Main.swift
3 | // Wandra
4 | //
5 | // Created by Mikael Löfgren on 2021-02-23.
6 | // Sound effects obtained from https://www.zapsplat.com
7 |
8 | import SwiftUI
9 |
10 | @main
11 | struct Main: App {
12 |
13 | var body: some Scene {
14 |
15 | WindowGroup {
16 | ContentView()
17 | // https://developer.apple.com/documentation/swiftui/commandgroupplacement
18 | }.commands {
19 | // Remove App menu items
20 | CommandGroup(replacing: .appInfo) {
21 | }
22 | CommandGroup(replacing: .systemServices) {
23 | }
24 | CommandGroup(replacing: .appVisibility) {
25 | }
26 | // Remove File menu items
27 | CommandGroup(replacing: .newItem) {
28 | }
29 | // Remove Edit menu items
30 | CommandGroup(replacing: .undoRedo) {
31 | }
32 | // Remove Window menu items Bring to front
33 | CommandGroup(replacing: .windowList) {
34 | }
35 | CommandGroup(replacing: .windowArrangement) {
36 | }
37 | CommandGroup(replacing: .windowSize) {
38 | }
39 | // Remove Help menu items
40 | CommandGroup(replacing: .help) {
41 | }
42 | }
43 | // Hide the Title Bar
44 | .windowStyle(HiddenTitleBarWindowStyle())
45 |
46 | // Show the Title Bar but no Title, kept as reference
47 | //.windowToolbarStyle(UnifiedWindowToolbarStyle(showsTitle: false))
48 | }
49 | }
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/Wandra/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Wandra/Wandra.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.files.user-selected.read-write
8 |
9 | com.apple.security.network.client
10 |
11 | com.apple.security.personal-information.location
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/Wandra/sonar.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mikaellofgren/wandra/15e1ebfc1d3c2784c29bb441a68a4901564ab5d4/Wandra/sonar.mp3
--------------------------------------------------------------------------------
/images/preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mikaellofgren/wandra/15e1ebfc1d3c2784c29bb441a68a4901564ab5d4/images/preview.png
--------------------------------------------------------------------------------
/images/wandra-ikon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mikaellofgren/wandra/15e1ebfc1d3c2784c29bb441a68a4901564ab5d4/images/wandra-ikon.png
--------------------------------------------------------------------------------