├── .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 --------------------------------------------------------------------------------