1
0
mirror of https://github.com/nicoverbruggen/phpmon.git synced 2025-08-07 20:10:08 +02:00

🚀 Version 2.5

This commit is contained in:
2020-11-27 16:32:26 +01:00
16 changed files with 143 additions and 104 deletions

1
.gitignore vendored
View File

@ -2,3 +2,4 @@ phpmon.xcodeproj/project.xcworkspace
phpmon.xcodeproj/xcuserdata
PHP Monitor.xcodeproj/project.xcworkspace
PHP Monitor.xcodeproj/xcuserdata
.DS_Store

View File

@ -9,6 +9,7 @@
/* Begin PBXBuildFile section */
C405A4D024B9B9140062FAFA /* InternetAccessPolicy.strings in Resources */ = {isa = PBXBuildFile; fileRef = C405A4CE24B9B9130062FAFA /* InternetAccessPolicy.strings */; };
C405A4D124B9B9140062FAFA /* InternetAccessPolicy.plist in Resources */ = {isa = PBXBuildFile; fileRef = C405A4CF24B9B9140062FAFA /* InternetAccessPolicy.plist */; };
C412E5FC25700D5300A1FB67 /* HomebrewPackage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */; };
C41C1B3722B0097F00E7CF16 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */; };
C41C1B3B22B0098000E7CF16 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C41C1B3A22B0098000E7CF16 /* Assets.xcassets */; };
C41C1B3E22B0098000E7CF16 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C41C1B3C22B0098000E7CF16 /* Main.storyboard */; };
@ -32,6 +33,7 @@
/* Begin PBXFileReference section */
C405A4CE24B9B9130062FAFA /* InternetAccessPolicy.strings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; path = InternetAccessPolicy.strings; sourceTree = "<group>"; };
C405A4CF24B9B9140062FAFA /* InternetAccessPolicy.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = InternetAccessPolicy.plist; sourceTree = "<group>"; };
C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomebrewPackage.swift; sourceTree = "<group>"; };
C41C1B3322B0097F00E7CF16 /* PHP Monitor.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "PHP Monitor.app"; sourceTree = BUILT_PRODUCTS_DIR; };
C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
C41C1B3A22B0098000E7CF16 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
@ -51,6 +53,8 @@
C4811D2322D70A4700B5F6B3 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; };
C4811D2922D70F9A00B5F6B3 /* MainMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainMenu.swift; sourceTree = "<group>"; };
C4D8016522B1584700C6DA1B /* Startup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Startup.swift; sourceTree = "<group>"; };
C4E713562570150F00007428 /* SECURITY.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = SECURITY.md; sourceTree = "<group>"; };
C4E713572570151400007428 /* docs */ = {isa = PBXFileReference; lastKnownFileType = folder; path = docs; sourceTree = "<group>"; };
C4EE188322D3386B00E126E5 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
C4F8C0A322D4F12C002EFE61 /* DateExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtension.swift; sourceTree = "<group>"; };
C4F8C0A522D4FA41002EFE61 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
@ -80,6 +84,8 @@
isa = PBXGroup;
children = (
C4F8C0A522D4FA41002EFE61 /* README.md */,
C4E713562570150F00007428 /* SECURITY.md */,
C4E713572570151400007428 /* docs */,
C41C1B3522B0097F00E7CF16 /* phpmon */,
C41C1B3422B0097F00E7CF16 /* Products */,
);
@ -164,6 +170,7 @@
C41C1B4A22B019FF00E7CF16 /* PhpVersion.swift */,
C41C1B4822B00A9800E7CF16 /* MenuBarImageGenerator.swift */,
C474B00524C0E98C00066A22 /* LocalNotification.swift */,
C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */,
);
path = Helpers;
sourceTree = "<group>";
@ -258,6 +265,7 @@
C4811D2422D70A4700B5F6B3 /* App.swift in Sources */,
C41C1B4922B00A9800E7CF16 /* MenuBarImageGenerator.swift in Sources */,
C4811D2A22D70F9A00B5F6B3 /* MainMenu.swift in Sources */,
C412E5FC25700D5300A1FB67 /* HomebrewPackage.swift in Sources */,
C41C1B3722B0097F00E7CF16 /* AppDelegate.swift in Sources */,
C41C1B4B22B019FF00E7CF16 /* PhpVersion.swift in Sources */,
C476FF9822B0DD830098105B /* Alert.swift in Sources */,
@ -407,7 +415,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 25;
CURRENT_PROJECT_VERSION = 26;
DEVELOPMENT_TEAM = 8M54J5J787;
ENABLE_HARDENED_RUNTIME = YES;
INFOPLIST_FILE = phpmon/Info.plist;
@ -415,7 +423,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 2.4;
MARKETING_VERSION = 2.5;
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -431,7 +439,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 25;
CURRENT_PROJECT_VERSION = 26;
DEVELOPMENT_TEAM = 8M54J5J787;
ENABLE_HARDENED_RUNTIME = YES;
INFOPLIST_FILE = phpmon/Info.plist;
@ -439,7 +447,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 2.4;
MARKETING_VERSION = 2.5;
PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";

View File

@ -6,7 +6,7 @@ PHP Monitor (or phpmon) is a lightweight macOS utility app that runs on your Mac
It also gives you quick access to various useful functionality (like switching PHP versions, restarting services, accessing configuration files, and more).
<img src="./docs/screenshot.png" width="362px" alt="phpmon screenshot"/>
<img src="./docs/screenshot.png" width="370px" alt="phpmon screenshot"/>
For me, it comes in handy when running multiple versions of PHP with Homebrew. If you wish to be able to see at a glance which version is currently linked & active with Laravel Valet, PHP Monitor is your new best friend.
@ -14,13 +14,13 @@ It's also super convenient to switch between different versions of PHP, or to fi
## 🖥 System requirements
PHP Monitor is a universal application that runs on Apple Silicon *and* Intel-based Macs.
PHP Monitor is a universal application that runs on Apple Silicon **and** Intel-based Macs.
* macOS 10.15 Catalina or higher (works on macOS 11 Big Sur)
* PHP 7.4 installed with Homebrew 2.x
* The brew formula `php` has to be installed (which version it is, is detected)
* Laravel Valet 2.x
_Please note that future versions of PHP will not work automatically, minor changes are required to add support for newer versions of PHP._
_Please note that future versions of PHP will not work automatically, minor changes are usually required to add support for newer versions of PHP. You may need to update your Valet installation to keep everything working if a major version update of PHP has been released._
## 🚀 How to install
@ -51,14 +51,14 @@ This utility will detect which PHP versions you have installed via Homebrew, and
This means:
- You have at least the latest version of PHP installed (`php@7.4`)
- You have at least the latest version of PHP installed (`php`)
- You have installed Laravel Valet (`which valet` returns `/usr/local/bin/valet`)
- You ran `valet trust`, which means Valet commands can be run without using sudo
The utility runs the following commands:
- Unlink all detected PHP versions
- Switch to PHP 7.4 (this is done to ensure that Valet works, even when attempting to use PHP 5.6)
- Switch to whatever version of PHP `php` is at (this is done to ensure that Valet works, even when attempting to use PHP 5.6)
- Stop all php-fpm service instances
- Link the desired version of PHP
- Start the correct php-fpm service for the desired PHP version
@ -71,12 +71,12 @@ This app isn't very complicated after all. In the end, this just (conveniently)
## 🤬 Troubleshooting
**If you are having issues, the first thing you should be doing is installing the latest version of PHP Monitor. This can resolve a variety of issues.**
**If you are having issues, the first thing you should be doing is installing the latest version of PHP Monitor _and_ Laravel Valet. This can resolve a variety of issues. Don't forget to run `valet install` after upgrading.**
PHP Monitor performs some integrity checks to ensure a good experience when using the app. You'll get a message telling you that PHP Monitor won't work correctly in the following scenarios:
- The PHP binary is not located in `/usr/local/bin/php`
- PHP 7.4 is missing in `/usr/local/opt`
- PHP is missing in `/usr/local/opt`
- Laravel Valet is missing in `/usr/local/bin/valet`
- Brew has not been added to sudoers in `/private/etc/sudoers.d/brew`
- Valet has not been added to sudoers in `/private/etc/sudoers.d/valet`
@ -84,9 +84,9 @@ PHP Monitor performs some integrity checks to ensure a good experience when usin
Follow instructions as specified in the alert in order to resolve any issues.
## 📝 Additional information
## 📝 Additional information & FAQ
Please consult the [additional information][2] file that contains more information.
Please consult the [additional information][2] file that contains more information. It has answers to additional questions and more information to troubleshoot your problem.
## ⭐️ Star me!

View File

@ -6,7 +6,8 @@ The following versions of PHP Monitor are supported:
| Version | Universal | Supported | Runs on macOS |
| ------- | ------------- | ------------------ | ----- |
| 2.4 | ✅ | ✅ | Catalina (10.15), Big Sur (11.0) |
| 2.5 | ✅ | ✅ | Catalina (10.15), Big Sur (11.0) |
| 2.4 | ✅ | ❌ | Catalina (10.15), Big Sur (11.0) |
| < 2.4 | | | Catalina (10.15) |
## Reporting a Vulnerability

View File

@ -1,6 +1,14 @@
### Q&A
#### Q: This app is doing network requests?
#### Q: Does this support Apple Silicon?
Yes. This is a universal app.
#### Q: Is PHP 8.x supported?
Yes.
#### Q: This app is doing network requests? Why?
It's Homebrew. I can't prevent `brew` from doing things via the network when I invoke it.
@ -49,10 +57,14 @@ Super convenient!
#### Q: PHP Monitor says that the latest version of PHP is not installed, but it is!
Try installing again using `brew install php@7.4`.
Try installing again using `brew install php`.
This should resolve the issue.
#### Q: PHP Monitor says the correct version is loaded, but my Valet sites don't work!
You may need to run `valet install`. (Preferably after updating `valet` by running `composer global update`).
#### Q: PHP Monitor reports another version compared to phpinfo on my local website, what is going on?
_Beginning with version 2.0 you'll get alerts about this at startup._

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 KiB

After

Width:  |  Height:  |  Size: 270 KiB

View File

@ -14,21 +14,32 @@ class Actions {
public static func detectPhpVersions() -> [String] {
let files = Shell.user.pipe("ls /usr/local/opt | grep php@")
var versions = files.components(separatedBy: "\n")
// Remove all empty strings
versions.removeAll { (string) -> Bool in
return (string == "")
}
// Get a list of versions only
var versionsOnly : [String] = []
versions.forEach { (string) in
versionsOnly.append(string.components(separatedBy: "php@")[1])
}
// Make sure the aliased version is detected
// The user may have `php` installed, but not e.g. `php@8.0`
// We should also detect that as a version that is installed
let phpAlias = App.shared.brewPhpVersion
if (!versionsOnly.contains(phpAlias)) {
versionsOnly.append(phpAlias);
}
return versionsOnly
}
public static func restartPhpFpm() {
let version = App.shared.currentVersion!.short
if (version == Constants.LatestPhpVersion) {
if (version == App.shared.brewPhpVersion) {
Shell.user.run("sudo brew services restart php")
} else {
Shell.user.run("sudo brew services restart php@\(version)")
@ -45,16 +56,16 @@ class Actions {
// Unlink the current version
Shell.user.run("brew unlink php@\(version)")
// Stop the services
if (version == Constants.LatestPhpVersion) {
if (version == App.shared.brewPhpVersion) {
Shell.user.run("sudo brew services stop php")
} else {
Shell.user.run("sudo brew services stop php@\(version)")
}
}
if (availableVersions.contains(Constants.LatestPhpVersion)) {
if (availableVersions.contains(App.shared.brewPhpVersion)) {
// Use the latest version as a default
Shell.user.run("brew link php@\(Constants.LatestPhpVersion) --overwrite --force")
if (version == Constants.LatestPhpVersion) {
Shell.user.run("brew link php@\(App.shared.brewPhpVersion) --overwrite --force")
if (version == App.shared.brewPhpVersion) {
// If said version was also requested, all we need to do is start the service
Shell.user.run("sudo brew services start php")
} else {
@ -115,7 +126,7 @@ class Actions {
let versions = self.detectPhpVersions()
versions.forEach { (version) in
Shell.user.run("brew unlink php@\(version)")
if (version == Constants.LatestPhpVersion) {
if (version == App.shared.brewPhpVersion) {
Shell.user.run("brew services stop php")
Shell.user.run("sudo brew services stop php")
} else {

View File

@ -32,9 +32,9 @@ class Startup {
)
self.performEnvironmentCheck(
!Shell.user.pipe("ls /usr/local/opt | grep php@7.4").contains("php@7.4"),
messageText: "PHP 7.4 is not correctly installed",
informativeText: "PHP 7.4 alias was not found in `/usr/local/opt`. The app will not work correctly until you resolve this issue. If you already have the `php` formula installed, you may need to run `brew install php@7.4` in order for PHP Monitor to detect this installation.",
!Shell.user.pipe("ls /usr/local/opt | grep php").contains("php"),
messageText: "PHP is not correctly installed",
informativeText: "PHP alias was not found in `/usr/local/opt`. The app will not work correctly until you resolve this issue. If you already have the `php` formula installed, you may need to run `brew install php` in order for PHP Monitor to detect this installation.",
breaking: true
)
@ -72,10 +72,30 @@ class Startup {
)
if (!self.failed) {
self.determineBrewAliasVersion()
success()
}
}
/**
* In order to avoid having to hard-code which version of PHP is aliased to what specific subversion,
* PHP Monitor now determines the alias by checking the user's system.
*/
private func determineBrewAliasVersion()
{
print("PHP Monitor has determined the application has successfully passed all checks.")
print("Determining which version of PHP is aliased to `php` via Homebrew...")
let brewPhpAlias = Shell.user.pipe("brew info php --json");
App.shared.brewPhpPackage = try! JSONDecoder().decode(
[HomebrewPackage].self,
from: brewPhpAlias.data(using: .utf8)!
).first!
print("When on your system, the `php` formula means version \(App.shared.brewPhpVersion)!")
}
/**
* Perform an environment check. Will cause the application to terminate, if `breaking` is set to true.
*

View File

@ -0,0 +1,19 @@
//
// HomebrewPackage.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 26/11/2020.
// Copyright © 2020 Nico Verbruggen. All rights reserved.
//
import Foundation
struct HomebrewPackage : Decodable {
let name: String
let full_name: String
let aliases: [String]
public func getVersion() -> String {
return aliases.first!.replacingOccurrences(of: "php@", with: "")
}
}

View File

@ -35,7 +35,8 @@ class StatusMenu : NSMenu {
for index in (0..<App.shared.availablePhpVersions.count).reversed() {
let version = App.shared.availablePhpVersions[index]
let action = #selector(MainMenu.switchToPhpVersion(sender:))
let menuItem = NSMenuItem(title: "\("mi_php_switch".localized) \(version)", action: (version == App.shared.currentVersion?.short) ? nil : action, keyEquivalent: "\(shortcutKey)")
let brew = (version == App.shared.brewPhpVersion) ? "php" : "php@\(version)"
let menuItem = NSMenuItem(title: "\("mi_php_switch".localized) \(version) (\(brew))", action: (version == App.shared.currentVersion?.short) ? nil : action, keyEquivalent: "\(shortcutKey)")
menuItem.tag = index
shortcutKey = shortcutKey + 1
self.addItem(menuItem)
@ -57,6 +58,7 @@ class StatusMenu : NSMenu {
self.addItem(NSMenuItem(title: "mi_configuration".localized, action: nil, keyEquivalent: ""))
self.addItem(NSMenuItem(title: "mi_valet_config".localized, action: #selector(MainMenu.openValetConfigFolder), keyEquivalent: "v"))
self.addItem(NSMenuItem(title: "mi_php_config".localized, action: #selector(MainMenu.openActiveConfigFolder), keyEquivalent: "c"))
self.addItem(NSMenuItem(title: "mi_phpinfo".localized, action: #selector(MainMenu.openPhpInfo), keyEquivalent: "i"))
self.addItem(NSMenuItem.separator())
self.addItem(NSMenuItem(title: "mi_enabled_extensions".localized, action: nil, keyEquivalent: ""))
self.addXdebugMenuItem()

View File

@ -12,15 +12,17 @@ class Constants {
/**
* The PHP versions supported by this application.
* Versions that do not appear in this array are omitted from the list.
*/
static let SupportedPhpVersions = [
"5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0"
"5.6",
"7.0",
"7.1",
"7.2",
"7.3",
"7.4",
"8.0",
"8.1"
]
/**
Which php version is aliased as `php` to brew?
This is usually the latest PHP version.
*/
static let LatestPhpVersion = "7.4"
}

View File

@ -38,7 +38,7 @@
<key>Relevance</key>
<string>Essential</string>
<key>Purpose</key>
<string>PHP Monitor directly invokes Homebrew which contacts GitHub.</string>
<string>PHP Monitor directly invokes Homebrew which contacts GitHub. This happens when PHP Monitor asks for more information about the PHP formula to determine which version of PHP you&apos;ve got running.</string>
<key>DenyConsequences</key>
<string>If you deny these connections, PHP Monitor might not be able to complete its preset set of instructions, causing version switching to fail.</string>
</dict>

View File

@ -23,8 +23,9 @@
"mi_force_load_latest" = "Force load latest PHP version";
"mi_configuration" = "Configuration";
"mi_valet_config" = "Valet configuration (.config/valet)";
"mi_php_config" = "PHP Configuration file (php.ini)";
"mi_valet_config" = "Locate Valet folder (.config/valet)";
"mi_php_config" = "Locate PHP configuration file (php.ini)";
"mi_phpinfo" = "Show current configuration (phpinfo)";
"mi_enabled_extensions" = "Enabled Extensions";
"mi_xdebug" = "Xdebug";

View File

@ -32,4 +32,24 @@ class App {
*/
var timer: Timer?
/**
Information we were able to discern from the Homebrew info command (as JSON).
*/
var brewPhpPackage: HomebrewPackage? = nil {
didSet {
self.brewPhpVersion = self.brewPhpPackage!.getVersion()
}
}
/**
The version that the `php` formula via Brew is aliased to on the current system.
If you're up to date, `php` will be aliased to the latest version,
but that might not be the case.
We'll technically default to version 8.0, but the information should always be loaded
from Homebrew itself upon starting the application.
*/
var brewPhpVersion: String = "8.0"
}

View File

@ -198,6 +198,12 @@ class MainMenu: NSObject, NSWindowDelegate {
})
}
@objc public func openPhpInfo() {
try! "<?php phpinfo();".write(toFile: "/tmp/phpmon_phpinfo.php", atomically: true, encoding: .utf8)
Shell.user.run("/usr/local/bin/php-cgi -q /tmp/phpmon_phpinfo.php > /tmp/phpmon_phpinfo.html")
NSWorkspace.shared.open(URL(string: "file:///private/tmp/phpmon_phpinfo.html")!)
}
@objc public func forceRestartLatestPhp() {
// Tell the user the switch is about to occur
_ = Alert.present(

View File

@ -1,9 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="16096" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="17506" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="16096"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="17506"/>
</dependencies>
<scenes>
<!--Application-->
@ -55,68 +54,5 @@
</objects>
<point key="canvasLocation" x="-343" y="-16"/>
</scene>
<!--Log View Controller-->
<scene sceneID="hIz-AP-VOD">
<objects>
<viewController storyboardIdentifier="logWindow" id="XfG-lQ-9wD" customClass="LogViewController" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" identifier="main" id="m2S-Jp-Qdl">
<rect key="frame" x="0.0" y="0.0" width="662" height="475"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ICa-gx-jgq">
<rect key="frame" x="578" y="8" width="75" height="32"/>
<buttonCell key="cell" type="push" title="Close" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="3md-FI-EWa">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="pressed:" target="XfG-lQ-9wD" id="fIC-Bz-vTK"/>
</connections>
</button>
<scrollView borderType="line" horizontalLineScroll="10" horizontalPageScroll="10" verticalLineScroll="10" verticalPageScroll="10" hasHorizontalScroller="NO" translatesAutoresizingMaskIntoConstraints="NO" id="vZy-5S-021">
<rect key="frame" x="15" y="46" width="632" height="414"/>
<clipView key="contentView" drawsBackground="NO" copiesOnScroll="NO" id="s5L-AU-0fw">
<rect key="frame" x="1" y="1" width="630" height="412"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textView importsGraphics="NO" richText="NO" verticallyResizable="YES" smartInsertDelete="YES" id="tN6-Y9-1pA">
<rect key="frame" x="0.0" y="0.0" width="630" height="412"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
<size key="minSize" width="630" height="412"/>
<size key="maxSize" width="640" height="10000000"/>
<color key="insertionPointColor" name="textColor" catalog="System" colorSpace="catalog"/>
</textView>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</clipView>
<scroller key="horizontalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="YES" id="Kho-JF-NZJ">
<rect key="frame" x="-100" y="-100" width="240" height="16"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
<scroller key="verticalScroller" wantsLayer="YES" verticalHuggingPriority="750" horizontal="NO" id="qp7-7R-gTO">
<rect key="frame" x="615" y="1" width="16" height="412"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
</scrollView>
</subviews>
<constraints>
<constraint firstItem="vZy-5S-021" firstAttribute="leading" secondItem="m2S-Jp-Qdl" secondAttribute="leading" constant="15" id="K0k-oE-r37"/>
<constraint firstAttribute="trailing" secondItem="ICa-gx-jgq" secondAttribute="trailing" constant="15" id="LFS-0E-Ibw"/>
<constraint firstItem="vZy-5S-021" firstAttribute="top" secondItem="m2S-Jp-Qdl" secondAttribute="top" constant="15" id="Nec-oI-CjE"/>
<constraint firstAttribute="trailing" secondItem="vZy-5S-021" secondAttribute="trailing" constant="15" id="kBJ-O5-eYI"/>
<constraint firstAttribute="bottom" secondItem="ICa-gx-jgq" secondAttribute="bottom" constant="15" id="kYB-Fn-DSA"/>
<constraint firstItem="ICa-gx-jgq" firstAttribute="top" secondItem="vZy-5S-021" secondAttribute="bottom" constant="10" id="xdn-yU-LVb"/>
</constraints>
</view>
<connections>
<outlet property="textView" destination="tN6-Y9-1pA" id="z77-me-Od6"/>
</connections>
</viewController>
<customObject id="rPt-NT-nkU" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-105" y="377.5"/>
</scene>
</scenes>
</document>