├── .gitignore
├── LICENSE.md
├── README.md
├── Settings.bundle
├── Root.plist
└── en.lproj
│ └── Root.strings
├── chatr.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── swiftpm
│ │ └── Package.resolved
└── xcshareddata
│ └── xcschemes
│ └── chatr.xcscheme
├── chatr
├── AboutUsPage.swift
├── AppDelegate.swift
├── Assets.xcassets
│ ├── AppIcon.appiconset
│ │ ├── Contents.json
│ │ ├── DM (2).jpg
│ │ ├── Icon-App-20x20@1x.png
│ │ ├── Icon-App-20x20@2x-1.png
│ │ ├── Icon-App-20x20@2x.png
│ │ ├── Icon-App-20x20@3x.png
│ │ ├── Icon-App-29x29@1x.png
│ │ ├── Icon-App-29x29@2x-1.png
│ │ ├── Icon-App-29x29@2x.png
│ │ ├── Icon-App-29x29@3x.png
│ │ ├── Icon-App-40x40@1x.png
│ │ ├── Icon-App-40x40@2x-1.png
│ │ ├── Icon-App-40x40@2x.png
│ │ ├── Icon-App-40x40@3x.png
│ │ ├── Icon-App-60x60@2x.png
│ │ ├── Icon-App-60x60@3x.png
│ │ ├── Icon-App-76x76@1x.png
│ │ ├── Icon-App-76x76@2x.png
│ │ └── Icon-App-83.5x83.5@2x.png
│ ├── ChatPage.swift
│ ├── Contents.json
│ ├── backarrow.imageset
│ │ ├── Contents.json
│ │ └── backarrow.png
│ ├── information.imageset
│ │ ├── Contents.json
│ │ └── Picture1.png
│ ├── logo.imageset
│ │ ├── Contents.json
│ │ ├── iTunesArtwork@2x.png
│ │ ├── iTunesArtwork@3x.png
│ │ └── logo.png
│ ├── menubar.imageset
│ │ ├── Contents.json
│ │ └── DM-3.jpg
│ ├── print.imageset
│ │ ├── Contents.json
│ │ └── print - graphic.png
│ ├── profile.imageset
│ │ ├── Contents.json
│ │ └── person - graphic.png
│ ├── save.imageset
│ │ ├── Contents.json
│ │ └── save - graphic.png
│ ├── settings.imageset
│ │ ├── Contents.json
│ │ └── settings-graphic.png
│ └── sidebar.imageset
│ │ ├── Contents.json
│ │ └── DM-3.jpg
├── Base.lproj
│ ├── LaunchScreen.storyboard
│ └── Main.storyboard
├── ChatPage.swift
├── ChatTableViewCell.swift
├── EnrollmentDelegate.swift
├── Info.plist
├── KeychainManager.swift
├── LoginPage.swift
├── PolicyDelegate.swift
├── SettingsPage.swift
├── SideBarTableCell.swift
├── UIUtils.swift
└── chatr.entitlements
└── logo.jpg
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | xcuserdata/
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Microsoft Intune
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 | # Chatr - An Intune MAM iOS SDK Example (Swift)
2 | This application is a demonstration of the [Microsoft Intune SDK for iOS](https://github.com/msintuneappsdk/ms-intune-app-sdk-ios). A developer guide to the SDK is available [here](https://learn.microsoft.com/mem/intune/developer/app-sdk-ios). This project implements some commonly used features so developers integrating their apps with the SDK have an example to follow.
3 |
4 | Chatr offers a simple messaging interface allowing users to send messages, print, and save conversations to their local device. It uses the [Microsoft Authentication Library](https://github.com/AzureAD/microsoft-authentication-library-for-objc) to authenticate users.
5 |
6 | ## Steps to run the app
7 | In order to deploy this sample you will need an Intune subscription. Free trials are sufficient for this demo.
8 |
9 | ### Step 1: Setting up Intune
10 | You will need at least one user assigned to a user group. You can see how to create new users [here](https://learn.microsoft.com/mem/intune/fundamentals/quickstart-create-user) and user groups [here](https://learn.microsoft.com/mem/intune/fundamentals/quickstart-create-group). Be sure to assign Intune licenses to your users [here](https://learn.microsoft.com/mem/intune/fundamentals/quickstart-create-user#assign-a-license-to-an-individual-user).
11 |
12 | ### Step 2: Create and Deploy App Protection Policy (APP)
13 | To enable MAM without device enrollment (MAM-WE), we must create a new App Protection Policy with Intune. Instructions for creating and deploying a new APP can be found [here](https://learn.microsoft.com/mem/intune/apps/quickstart-create-assign-app-policy).
14 | 1. To create an APP targeting the Chatr sample app, click on **Create policy** in the "App protection policies" pane.
15 | 1. In the "Create policy" pane specify the protection policy name, description, and platform. Click on **Select required apps**.
16 | 1. At the top of the pane click **More apps** and scroll to the bottom of the pane.
17 | 1. Enter the Bundle ID of your app and click **Add**. The Chatr bundle ID can be found by selecting the project file in the Xcode project explorer, selecting the chatr target, and selecting the "General" tab. This app's bundle ID is `Intune.chatr`.
18 | 1. Hit **Select** at the bottom of the "Apps" pane.
19 | 1. Click the **Settings** button in the "Create policy" pane and set the policy settings you would like to apply to a user group for your app.
20 | 1. Once you have selected the settings click **OK** at the bottom of the Settings pane and then click **Create** at the bottom of the "Create policy" pane. Your app should now appear in the "App protection policies" pane.
21 |
22 | ### Step 3: Create and Deploy App Configuration Policy
23 | Instructions for creating and deploying a new App Configuration Policy can be found [here](https://learn.microsoft.com/mem/intune/apps/app-configuration-policies-use-ios).
24 | 1. To create an App Configuration Policy targeting the Chatr sample app, click on **Add** in the "App configuration policies" pane.
25 | 1. In the "Add configuration policy" pane specify the configuration policy name and description. Under "Device enrollment type" select **Managed apps**. Click on **Select the required app**.
26 | 1. At the top of the pane click **More apps** and scroll to the bottom of the pane.
27 | 1. Enter the Bundle ID of your app and click **OK**. The Chatr bundle ID can be found by selecting the project file in the Xcode project explorer, selecting the chatr target, and selecting the "General" tab. This app's Package ID is `Intune.chatr`.
28 | 1. Click the **Configuration settings** button in the "Add configuration policy" pane and set the key-value pair configuration you would like to apply to a user group for your app. For intance, to change the messaging group name on the Chat Page of the Chatr sample app to "Intune", you can create a configuration where the key is "GroupName" and the value is "Intune".
29 | 1. Once you have added the key-value pair configuration click **OK** at the bottom of the "Configuration" pane and then click **Add** at the bottom of the "Add configuration policy" pane. Your app should now appear in the "App configuration policies" pane.
30 |
31 | ### Step 4: Launch the App & Sign-In
32 | Chatr should now be properly configured with Intune. When prompted to sign in, use one of the users in the group used in Step 2 or Step 3.
33 | ## Relevant Files
34 | - `Chatr/LoginPage.swift` contains logic for authenticating and enrolling the user with Intune.
35 | - `Chatr/EnrollmentDelegate.swift` contains logic which responds to an Intune enrollment or unenrollment attempt.
36 | - `Chatr/PolicyDelegate.swift` contains logic for removing data for a specific user when a selective wipe command is received from the Intune MAM service and responding to when the Intune SDK needs to restart the application.
37 | - `Chatr/KeychainManager.swift` contains logic for adding, updating, and removing user data in the keychain.
38 | - `Chatr/ChatPage.swift`
39 | - Registers the application to receive notifications when an IT administrator updates app configuration or protection policies.
40 | - Contains all of the main functionality of the app.
41 | - `Chatr/SettingsPage.swift` contains option to display the Intune Diagnostics Console, which end users can use to help IT administrators and Microsoft support diagnose issues.
42 |
--------------------------------------------------------------------------------
/Settings.bundle/Root.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | StringsTable
6 | Root
7 | PreferenceSpecifiers
8 |
9 |
10 | Type
11 | PSToggleSwitchSpecifier
12 | Title
13 | Display Intune Diagnostic Console
14 | Key
15 | PSToggleSwitchSpecifier
16 | DefaultValue
17 |
18 |
19 |
20 | Type
21 | PSToggleSwitchSpecifier
22 | Title
23 | Intune Diagnostic Enabled
24 | Key
25 | IntuneDiagnosticEnabled
26 | DefaultValue
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/Settings.bundle/en.lproj/Root.strings:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/Settings.bundle/en.lproj/Root.strings
--------------------------------------------------------------------------------
/chatr.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 54;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 68AF9BE321E8E19300127E37 /* SettingsPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68AF9BE021E8E19200127E37 /* SettingsPage.swift */; };
11 | 68BD62D222039F2F00FC3F16 /* PolicyDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68BD62D122039F2F00FC3F16 /* PolicyDelegate.swift */; };
12 | 68BD62D422039F4700FC3F16 /* EnrollmentDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68BD62D322039F4700FC3F16 /* EnrollmentDelegate.swift */; };
13 | 68EE322C21F141C600C47FA1 /* KeychainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68EE322B21F141C600C47FA1 /* KeychainManager.swift */; };
14 | 68FECA9C2204CA1700540C42 /* UIUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68FECA9B2204CA1700540C42 /* UIUtils.swift */; };
15 | AC008E442D7A4C8900CBC247 /* MSAL in Frameworks */ = {isa = PBXBuildFile; productRef = AC008E432D7A4C8900CBC247 /* MSAL */; };
16 | AC124D5D246B30FC00409590 /* CoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AC124D5C246B30FC00409590 /* CoreServices.framework */; };
17 | AC7BDDA62D7A745F00C46F2D /* IntuneMAMSwift in Frameworks */ = {isa = PBXBuildFile; productRef = AC7BDDA52D7A745F00C46F2D /* IntuneMAMSwift */; };
18 | AC7BDDA82D7A745F00C46F2D /* IntuneMAMTelemetry in Frameworks */ = {isa = PBXBuildFile; productRef = AC7BDDA72D7A745F00C46F2D /* IntuneMAMTelemetry */; };
19 | C01A67E020DAB03F00F80AFB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C01A67DF20DAB03F00F80AFB /* AppDelegate.swift */; };
20 | C01A67E220DAB03F00F80AFB /* LoginPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C01A67E120DAB03F00F80AFB /* LoginPage.swift */; };
21 | C01A67E520DAB03F00F80AFB /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C01A67E320DAB03F00F80AFB /* Main.storyboard */; };
22 | C01A67E720DAB03F00F80AFB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C01A67E620DAB03F00F80AFB /* Assets.xcassets */; };
23 | C01A67EA20DAB03F00F80AFB /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C01A67E820DAB03F00F80AFB /* LaunchScreen.storyboard */; };
24 | C01A680E20DAE2BB00F80AFB /* logo.jpg in Resources */ = {isa = PBXBuildFile; fileRef = C01A680D20DAE2BB00F80AFB /* logo.jpg */; };
25 | C01A681320E166CA00F80AFB /* SideBarTableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C01A681220E166CA00F80AFB /* SideBarTableCell.swift */; };
26 | C027B3B120E687DF00392A38 /* ChatPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C027B3B020E687DF00392A38 /* ChatPage.swift */; };
27 | C0571FB420EAAB57008716A8 /* AboutUsPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0571FB320EAAB57008716A8 /* AboutUsPage.swift */; };
28 | C0E7DA0320EE766600EA7494 /* ChatTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0E7DA0220EE766600EA7494 /* ChatTableViewCell.swift */; };
29 | C0E7DA0A20FE897200EA7494 /* MessageUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0E7DA0920FE897200EA7494 /* MessageUI.framework */; };
30 | C0E7DA0C20FE897900EA7494 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0E7DA0B20FE897900EA7494 /* Security.framework */; };
31 | C0E7DA1020FE898D00EA7494 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0E7DA0F20FE898D00EA7494 /* SystemConfiguration.framework */; };
32 | C0E7DA1220FE899900EA7494 /* libsqlite3.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = C0E7DA1120FE899900EA7494 /* libsqlite3.tbd */; };
33 | C0E7DA1420FE89A300EA7494 /* libc++.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = C0E7DA1320FE89A300EA7494 /* libc++.tbd */; };
34 | C0E7DA1620FE89AD00EA7494 /* ImageIO.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0E7DA1520FE89AD00EA7494 /* ImageIO.framework */; };
35 | C0E7DA1820FE89B500EA7494 /* LocalAuthentication.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0E7DA1720FE89B500EA7494 /* LocalAuthentication.framework */; };
36 | C0E7DA1A20FE89C800EA7494 /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0E7DA1920FE89C800EA7494 /* AudioToolbox.framework */; };
37 | C0E7DA1C20FE89D400EA7494 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0E7DA1B20FE89D400EA7494 /* QuartzCore.framework */; };
38 | C0E7DA1E20FE89DB00EA7494 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0E7DA1D20FE89DB00EA7494 /* WebKit.framework */; };
39 | /* End PBXBuildFile section */
40 |
41 | /* Begin PBXFileReference section */
42 | 68AF9BE021E8E19200127E37 /* SettingsPage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsPage.swift; sourceTree = ""; };
43 | 68BD62D122039F2F00FC3F16 /* PolicyDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PolicyDelegate.swift; sourceTree = ""; };
44 | 68BD62D322039F4700FC3F16 /* EnrollmentDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnrollmentDelegate.swift; sourceTree = ""; };
45 | 68EE322B21F141C600C47FA1 /* KeychainManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainManager.swift; sourceTree = ""; };
46 | 68FECA9B2204CA1700540C42 /* UIUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIUtils.swift; sourceTree = ""; };
47 | AC124D5C246B30FC00409590 /* CoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreServices.framework; path = System/Library/Frameworks/CoreServices.framework; sourceTree = SDKROOT; };
48 | C01A67DC20DAB03F00F80AFB /* chatr.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = chatr.app; sourceTree = BUILT_PRODUCTS_DIR; };
49 | C01A67DF20DAB03F00F80AFB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
50 | C01A67E120DAB03F00F80AFB /* LoginPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginPage.swift; sourceTree = ""; };
51 | C01A67E420DAB03F00F80AFB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
52 | C01A67E620DAB03F00F80AFB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
53 | C01A67E920DAB03F00F80AFB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
54 | C01A67EB20DAB03F00F80AFB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
55 | C01A680D20DAE2BB00F80AFB /* logo.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; name = logo.jpg; path = ../logo.jpg; sourceTree = ""; };
56 | C01A681220E166CA00F80AFB /* SideBarTableCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideBarTableCell.swift; sourceTree = ""; };
57 | C027B3B020E687DF00392A38 /* ChatPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatPage.swift; sourceTree = ""; };
58 | C0571FB320EAAB57008716A8 /* AboutUsPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutUsPage.swift; sourceTree = ""; };
59 | C0E7DA0220EE766600EA7494 /* ChatTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatTableViewCell.swift; sourceTree = ""; };
60 | C0E7DA0920FE897200EA7494 /* MessageUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MessageUI.framework; path = System/Library/Frameworks/MessageUI.framework; sourceTree = SDKROOT; };
61 | C0E7DA0B20FE897900EA7494 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; };
62 | C0E7DA0F20FE898D00EA7494 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; };
63 | C0E7DA1120FE899900EA7494 /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = usr/lib/libsqlite3.tbd; sourceTree = SDKROOT; };
64 | C0E7DA1320FE89A300EA7494 /* libc++.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = "libc++.tbd"; path = "usr/lib/libc++.tbd"; sourceTree = SDKROOT; };
65 | C0E7DA1520FE89AD00EA7494 /* ImageIO.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ImageIO.framework; path = System/Library/Frameworks/ImageIO.framework; sourceTree = SDKROOT; };
66 | C0E7DA1720FE89B500EA7494 /* LocalAuthentication.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = LocalAuthentication.framework; path = System/Library/Frameworks/LocalAuthentication.framework; sourceTree = SDKROOT; };
67 | C0E7DA1920FE89C800EA7494 /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = System/Library/Frameworks/AudioToolbox.framework; sourceTree = SDKROOT; };
68 | C0E7DA1B20FE89D400EA7494 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; };
69 | C0E7DA1D20FE89DB00EA7494 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; };
70 | C0E7DA1F20FE8CAC00EA7494 /* chatr.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = chatr.entitlements; sourceTree = ""; };
71 | /* End PBXFileReference section */
72 |
73 | /* Begin PBXFrameworksBuildPhase section */
74 | C01A67D920DAB03F00F80AFB /* Frameworks */ = {
75 | isa = PBXFrameworksBuildPhase;
76 | buildActionMask = 2147483647;
77 | files = (
78 | C0E7DA1E20FE89DB00EA7494 /* WebKit.framework in Frameworks */,
79 | AC7BDDA82D7A745F00C46F2D /* IntuneMAMTelemetry in Frameworks */,
80 | AC7BDDA62D7A745F00C46F2D /* IntuneMAMSwift in Frameworks */,
81 | C0E7DA1C20FE89D400EA7494 /* QuartzCore.framework in Frameworks */,
82 | C0E7DA1A20FE89C800EA7494 /* AudioToolbox.framework in Frameworks */,
83 | C0E7DA1820FE89B500EA7494 /* LocalAuthentication.framework in Frameworks */,
84 | AC008E442D7A4C8900CBC247 /* MSAL in Frameworks */,
85 | AC124D5D246B30FC00409590 /* CoreServices.framework in Frameworks */,
86 | C0E7DA1620FE89AD00EA7494 /* ImageIO.framework in Frameworks */,
87 | C0E7DA1420FE89A300EA7494 /* libc++.tbd in Frameworks */,
88 | C0E7DA1220FE899900EA7494 /* libsqlite3.tbd in Frameworks */,
89 | C0E7DA1020FE898D00EA7494 /* SystemConfiguration.framework in Frameworks */,
90 | C0E7DA0C20FE897900EA7494 /* Security.framework in Frameworks */,
91 | C0E7DA0A20FE897200EA7494 /* MessageUI.framework in Frameworks */,
92 | );
93 | runOnlyForDeploymentPostprocessing = 0;
94 | };
95 | /* End PBXFrameworksBuildPhase section */
96 |
97 | /* Begin PBXGroup section */
98 | C01A67D320DAB03F00F80AFB = {
99 | isa = PBXGroup;
100 | children = (
101 | C01A67DE20DAB03F00F80AFB /* chatr */,
102 | C0E7DA0820FE897100EA7494 /* Frameworks */,
103 | C01A67DD20DAB03F00F80AFB /* Products */,
104 | );
105 | sourceTree = "";
106 | };
107 | C01A67DD20DAB03F00F80AFB /* Products */ = {
108 | isa = PBXGroup;
109 | children = (
110 | C01A67DC20DAB03F00F80AFB /* chatr.app */,
111 | );
112 | name = Products;
113 | sourceTree = "";
114 | };
115 | C01A67DE20DAB03F00F80AFB /* chatr */ = {
116 | isa = PBXGroup;
117 | children = (
118 | C0571FB320EAAB57008716A8 /* AboutUsPage.swift */,
119 | C01A67DF20DAB03F00F80AFB /* AppDelegate.swift */,
120 | C01A67E620DAB03F00F80AFB /* Assets.xcassets */,
121 | C027B3B020E687DF00392A38 /* ChatPage.swift */,
122 | C0E7DA1F20FE8CAC00EA7494 /* chatr.entitlements */,
123 | C0E7DA0220EE766600EA7494 /* ChatTableViewCell.swift */,
124 | 68BD62D322039F4700FC3F16 /* EnrollmentDelegate.swift */,
125 | C01A67EB20DAB03F00F80AFB /* Info.plist */,
126 | 68EE322B21F141C600C47FA1 /* KeychainManager.swift */,
127 | C01A67E820DAB03F00F80AFB /* LaunchScreen.storyboard */,
128 | C01A67E120DAB03F00F80AFB /* LoginPage.swift */,
129 | C01A680D20DAE2BB00F80AFB /* logo.jpg */,
130 | C01A67E320DAB03F00F80AFB /* Main.storyboard */,
131 | 68BD62D122039F2F00FC3F16 /* PolicyDelegate.swift */,
132 | 68AF9BE021E8E19200127E37 /* SettingsPage.swift */,
133 | C01A681220E166CA00F80AFB /* SideBarTableCell.swift */,
134 | 68FECA9B2204CA1700540C42 /* UIUtils.swift */,
135 | );
136 | path = chatr;
137 | sourceTree = "";
138 | };
139 | C0E7DA0820FE897100EA7494 /* Frameworks */ = {
140 | isa = PBXGroup;
141 | children = (
142 | C0E7DA1920FE89C800EA7494 /* AudioToolbox.framework */,
143 | AC124D5C246B30FC00409590 /* CoreServices.framework */,
144 | C0E7DA1520FE89AD00EA7494 /* ImageIO.framework */,
145 | C0E7DA1320FE89A300EA7494 /* libc++.tbd */,
146 | C0E7DA1120FE899900EA7494 /* libsqlite3.tbd */,
147 | C0E7DA1720FE89B500EA7494 /* LocalAuthentication.framework */,
148 | C0E7DA0920FE897200EA7494 /* MessageUI.framework */,
149 | C0E7DA1B20FE89D400EA7494 /* QuartzCore.framework */,
150 | C0E7DA0B20FE897900EA7494 /* Security.framework */,
151 | C0E7DA0F20FE898D00EA7494 /* SystemConfiguration.framework */,
152 | C0E7DA1D20FE89DB00EA7494 /* WebKit.framework */,
153 | );
154 | name = Frameworks;
155 | sourceTree = "";
156 | };
157 | /* End PBXGroup section */
158 |
159 | /* Begin PBXNativeTarget section */
160 | C01A67DB20DAB03F00F80AFB /* chatr */ = {
161 | isa = PBXNativeTarget;
162 | buildConfigurationList = C01A680420DAB03F00F80AFB /* Build configuration list for PBXNativeTarget "chatr" */;
163 | buildPhases = (
164 | C01A67D820DAB03F00F80AFB /* Sources */,
165 | C01A67D920DAB03F00F80AFB /* Frameworks */,
166 | C01A67DA20DAB03F00F80AFB /* Resources */,
167 | );
168 | buildRules = (
169 | );
170 | dependencies = (
171 | );
172 | name = chatr;
173 | productName = chatr;
174 | productReference = C01A67DC20DAB03F00F80AFB /* chatr.app */;
175 | productType = "com.apple.product-type.application";
176 | };
177 | /* End PBXNativeTarget section */
178 |
179 | /* Begin PBXProject section */
180 | C01A67D420DAB03F00F80AFB /* Project object */ = {
181 | isa = PBXProject;
182 | attributes = {
183 | BuildIndependentTargetsInParallel = YES;
184 | LastSwiftUpdateCheck = 0920;
185 | LastUpgradeCheck = 1610;
186 | ORGANIZATIONNAME = "Microsoft Intune";
187 | TargetAttributes = {
188 | C01A67DB20DAB03F00F80AFB = {
189 | CreatedOnToolsVersion = 9.2;
190 | LastSwiftMigration = 1020;
191 | ProvisioningStyle = Automatic;
192 | SystemCapabilities = {
193 | com.apple.Keychain = {
194 | enabled = 1;
195 | };
196 | };
197 | };
198 | };
199 | };
200 | buildConfigurationList = C01A67D720DAB03F00F80AFB /* Build configuration list for PBXProject "chatr" */;
201 | compatibilityVersion = "Xcode 8.0";
202 | developmentRegion = en;
203 | hasScannedForEncodings = 0;
204 | knownRegions = (
205 | en,
206 | Base,
207 | );
208 | mainGroup = C01A67D320DAB03F00F80AFB;
209 | packageReferences = (
210 | AC008E422D7A4C8900CBC247 /* XCRemoteSwiftPackageReference "microsoft-authentication-library-for-objc" */,
211 | AC7BDDA42D7A745F00C46F2D /* XCRemoteSwiftPackageReference "ms-intune-app-sdk-ios" */,
212 | );
213 | productRefGroup = C01A67DD20DAB03F00F80AFB /* Products */;
214 | projectDirPath = "";
215 | projectRoot = "";
216 | targets = (
217 | C01A67DB20DAB03F00F80AFB /* chatr */,
218 | );
219 | };
220 | /* End PBXProject section */
221 |
222 | /* Begin PBXResourcesBuildPhase section */
223 | C01A67DA20DAB03F00F80AFB /* Resources */ = {
224 | isa = PBXResourcesBuildPhase;
225 | buildActionMask = 2147483647;
226 | files = (
227 | C01A680E20DAE2BB00F80AFB /* logo.jpg in Resources */,
228 | C01A67EA20DAB03F00F80AFB /* LaunchScreen.storyboard in Resources */,
229 | C01A67E720DAB03F00F80AFB /* Assets.xcassets in Resources */,
230 | C01A67E520DAB03F00F80AFB /* Main.storyboard in Resources */,
231 | );
232 | runOnlyForDeploymentPostprocessing = 0;
233 | };
234 | /* End PBXResourcesBuildPhase section */
235 |
236 | /* Begin PBXSourcesBuildPhase section */
237 | C01A67D820DAB03F00F80AFB /* Sources */ = {
238 | isa = PBXSourcesBuildPhase;
239 | buildActionMask = 2147483647;
240 | files = (
241 | 68BD62D222039F2F00FC3F16 /* PolicyDelegate.swift in Sources */,
242 | 68AF9BE321E8E19300127E37 /* SettingsPage.swift in Sources */,
243 | C0571FB420EAAB57008716A8 /* AboutUsPage.swift in Sources */,
244 | C0E7DA0320EE766600EA7494 /* ChatTableViewCell.swift in Sources */,
245 | 68EE322C21F141C600C47FA1 /* KeychainManager.swift in Sources */,
246 | 68FECA9C2204CA1700540C42 /* UIUtils.swift in Sources */,
247 | 68BD62D422039F4700FC3F16 /* EnrollmentDelegate.swift in Sources */,
248 | C01A67E220DAB03F00F80AFB /* LoginPage.swift in Sources */,
249 | C01A67E020DAB03F00F80AFB /* AppDelegate.swift in Sources */,
250 | C01A681320E166CA00F80AFB /* SideBarTableCell.swift in Sources */,
251 | C027B3B120E687DF00392A38 /* ChatPage.swift in Sources */,
252 | );
253 | runOnlyForDeploymentPostprocessing = 0;
254 | };
255 | /* End PBXSourcesBuildPhase section */
256 |
257 | /* Begin PBXVariantGroup section */
258 | C01A67E320DAB03F00F80AFB /* Main.storyboard */ = {
259 | isa = PBXVariantGroup;
260 | children = (
261 | C01A67E420DAB03F00F80AFB /* Base */,
262 | );
263 | name = Main.storyboard;
264 | sourceTree = "";
265 | };
266 | C01A67E820DAB03F00F80AFB /* LaunchScreen.storyboard */ = {
267 | isa = PBXVariantGroup;
268 | children = (
269 | C01A67E920DAB03F00F80AFB /* Base */,
270 | );
271 | name = LaunchScreen.storyboard;
272 | sourceTree = "";
273 | };
274 | /* End PBXVariantGroup section */
275 |
276 | /* Begin XCBuildConfiguration section */
277 | C01A680220DAB03F00F80AFB /* Debug */ = {
278 | isa = XCBuildConfiguration;
279 | buildSettings = {
280 | ALWAYS_SEARCH_USER_PATHS = NO;
281 | CLANG_ANALYZER_NONNULL = YES;
282 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
283 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
284 | CLANG_CXX_LIBRARY = "libc++";
285 | CLANG_ENABLE_MODULES = YES;
286 | CLANG_ENABLE_OBJC_ARC = YES;
287 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
288 | CLANG_WARN_BOOL_CONVERSION = YES;
289 | CLANG_WARN_COMMA = YES;
290 | CLANG_WARN_CONSTANT_CONVERSION = YES;
291 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
292 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
293 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
294 | CLANG_WARN_EMPTY_BODY = YES;
295 | CLANG_WARN_ENUM_CONVERSION = YES;
296 | CLANG_WARN_INFINITE_RECURSION = YES;
297 | CLANG_WARN_INT_CONVERSION = YES;
298 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
299 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
300 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
301 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
302 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
303 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
304 | CLANG_WARN_STRICT_PROTOTYPES = YES;
305 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
306 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
307 | CLANG_WARN_UNREACHABLE_CODE = YES;
308 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
309 | CODE_SIGN_IDENTITY = "iPhone Developer";
310 | COPY_PHASE_STRIP = NO;
311 | DEBUG_INFORMATION_FORMAT = dwarf;
312 | ENABLE_BITCODE = NO;
313 | ENABLE_STRICT_OBJC_MSGSEND = YES;
314 | ENABLE_TESTABILITY = YES;
315 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
316 | GCC_C_LANGUAGE_STANDARD = gnu11;
317 | GCC_DYNAMIC_NO_PIC = NO;
318 | GCC_NO_COMMON_BLOCKS = YES;
319 | GCC_OPTIMIZATION_LEVEL = 0;
320 | GCC_PREPROCESSOR_DEFINITIONS = (
321 | "DEBUG=1",
322 | "$(inherited)",
323 | );
324 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
325 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
326 | GCC_WARN_UNDECLARED_SELECTOR = YES;
327 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
328 | GCC_WARN_UNUSED_FUNCTION = YES;
329 | GCC_WARN_UNUSED_VARIABLE = YES;
330 | IPHONEOS_DEPLOYMENT_TARGET = 12.2;
331 | MTL_ENABLE_DEBUG_INFO = YES;
332 | ONLY_ACTIVE_ARCH = YES;
333 | SDKROOT = iphoneos;
334 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
335 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
336 | SWIFT_VERSION = 5.0;
337 | TARGETED_DEVICE_FAMILY = 1;
338 | };
339 | name = Debug;
340 | };
341 | C01A680320DAB03F00F80AFB /* Release */ = {
342 | isa = XCBuildConfiguration;
343 | buildSettings = {
344 | ALWAYS_SEARCH_USER_PATHS = NO;
345 | CLANG_ANALYZER_NONNULL = YES;
346 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
347 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
348 | CLANG_CXX_LIBRARY = "libc++";
349 | CLANG_ENABLE_MODULES = YES;
350 | CLANG_ENABLE_OBJC_ARC = YES;
351 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
352 | CLANG_WARN_BOOL_CONVERSION = YES;
353 | CLANG_WARN_COMMA = YES;
354 | CLANG_WARN_CONSTANT_CONVERSION = YES;
355 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
356 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
357 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
358 | CLANG_WARN_EMPTY_BODY = YES;
359 | CLANG_WARN_ENUM_CONVERSION = YES;
360 | CLANG_WARN_INFINITE_RECURSION = YES;
361 | CLANG_WARN_INT_CONVERSION = YES;
362 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
363 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
364 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
365 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
366 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
367 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
368 | CLANG_WARN_STRICT_PROTOTYPES = YES;
369 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
370 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
371 | CLANG_WARN_UNREACHABLE_CODE = YES;
372 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
373 | CODE_SIGN_IDENTITY = "iPhone Developer";
374 | COPY_PHASE_STRIP = NO;
375 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
376 | ENABLE_BITCODE = NO;
377 | ENABLE_NS_ASSERTIONS = NO;
378 | ENABLE_STRICT_OBJC_MSGSEND = YES;
379 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
380 | GCC_C_LANGUAGE_STANDARD = gnu11;
381 | GCC_NO_COMMON_BLOCKS = YES;
382 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
383 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
384 | GCC_WARN_UNDECLARED_SELECTOR = YES;
385 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
386 | GCC_WARN_UNUSED_FUNCTION = YES;
387 | GCC_WARN_UNUSED_VARIABLE = YES;
388 | IPHONEOS_DEPLOYMENT_TARGET = 12.2;
389 | MTL_ENABLE_DEBUG_INFO = NO;
390 | SDKROOT = iphoneos;
391 | SWIFT_COMPILATION_MODE = wholemodule;
392 | SWIFT_OPTIMIZATION_LEVEL = "-O";
393 | SWIFT_VERSION = 5.0;
394 | TARGETED_DEVICE_FAMILY = 1;
395 | VALIDATE_PRODUCT = YES;
396 | };
397 | name = Release;
398 | };
399 | C01A680520DAB03F00F80AFB /* Debug */ = {
400 | isa = XCBuildConfiguration;
401 | buildSettings = {
402 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
403 | CLANG_ENABLE_MODULES = YES;
404 | CODE_SIGN_ENTITLEMENTS = chatr/chatr.entitlements;
405 | CODE_SIGN_STYLE = Automatic;
406 | FRAMEWORK_SEARCH_PATHS = (
407 | "$(inherited)",
408 | "$(PROJECT_DIR)",
409 | "$(SRCROOT)",
410 | );
411 | INFOPLIST_FILE = chatr/Info.plist;
412 | IPHONEOS_DEPLOYMENT_TARGET = 16.0;
413 | LD_RUNPATH_SEARCH_PATHS = (
414 | "$(inherited)",
415 | "@executable_path/Frameworks",
416 | );
417 | ONLY_ACTIVE_ARCH = NO;
418 | PRODUCT_BUNDLE_IDENTIFIER = Intune.chatr;
419 | PRODUCT_NAME = "$(TARGET_NAME)";
420 | SWIFT_OBJC_BRIDGING_HEADER = "";
421 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
422 | SWIFT_VERSION = 5.0;
423 | TARGETED_DEVICE_FAMILY = "1,2";
424 | };
425 | name = Debug;
426 | };
427 | C01A680620DAB03F00F80AFB /* Release */ = {
428 | isa = XCBuildConfiguration;
429 | buildSettings = {
430 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
431 | CLANG_ENABLE_MODULES = YES;
432 | CODE_SIGN_ENTITLEMENTS = chatr/chatr.entitlements;
433 | CODE_SIGN_STYLE = Automatic;
434 | FRAMEWORK_SEARCH_PATHS = (
435 | "$(inherited)",
436 | "$(PROJECT_DIR)",
437 | "$(SRCROOT)",
438 | );
439 | INFOPLIST_FILE = chatr/Info.plist;
440 | IPHONEOS_DEPLOYMENT_TARGET = 16.0;
441 | LD_RUNPATH_SEARCH_PATHS = (
442 | "$(inherited)",
443 | "@executable_path/Frameworks",
444 | );
445 | ONLY_ACTIVE_ARCH = NO;
446 | PRODUCT_BUNDLE_IDENTIFIER = Intune.chatr;
447 | PRODUCT_NAME = "$(TARGET_NAME)";
448 | SWIFT_OBJC_BRIDGING_HEADER = "";
449 | SWIFT_VERSION = 5.0;
450 | TARGETED_DEVICE_FAMILY = "1,2";
451 | };
452 | name = Release;
453 | };
454 | /* End XCBuildConfiguration section */
455 |
456 | /* Begin XCConfigurationList section */
457 | C01A67D720DAB03F00F80AFB /* Build configuration list for PBXProject "chatr" */ = {
458 | isa = XCConfigurationList;
459 | buildConfigurations = (
460 | C01A680220DAB03F00F80AFB /* Debug */,
461 | C01A680320DAB03F00F80AFB /* Release */,
462 | );
463 | defaultConfigurationIsVisible = 0;
464 | defaultConfigurationName = Release;
465 | };
466 | C01A680420DAB03F00F80AFB /* Build configuration list for PBXNativeTarget "chatr" */ = {
467 | isa = XCConfigurationList;
468 | buildConfigurations = (
469 | C01A680520DAB03F00F80AFB /* Debug */,
470 | C01A680620DAB03F00F80AFB /* Release */,
471 | );
472 | defaultConfigurationIsVisible = 0;
473 | defaultConfigurationName = Release;
474 | };
475 | /* End XCConfigurationList section */
476 |
477 | /* Begin XCRemoteSwiftPackageReference section */
478 | AC008E422D7A4C8900CBC247 /* XCRemoteSwiftPackageReference "microsoft-authentication-library-for-objc" */ = {
479 | isa = XCRemoteSwiftPackageReference;
480 | repositoryURL = "https://github.com/AzureAD/microsoft-authentication-library-for-objc.git";
481 | requirement = {
482 | kind = upToNextMajorVersion;
483 | minimumVersion = 1.7.0;
484 | };
485 | };
486 | AC7BDDA42D7A745F00C46F2D /* XCRemoteSwiftPackageReference "ms-intune-app-sdk-ios" */ = {
487 | isa = XCRemoteSwiftPackageReference;
488 | repositoryURL = "https://github.com/microsoftconnect/ms-intune-app-sdk-ios.git";
489 | requirement = {
490 | kind = upToNextMajorVersion;
491 | minimumVersion = 20.4.0;
492 | };
493 | };
494 | /* End XCRemoteSwiftPackageReference section */
495 |
496 | /* Begin XCSwiftPackageProductDependency section */
497 | AC008E432D7A4C8900CBC247 /* MSAL */ = {
498 | isa = XCSwiftPackageProductDependency;
499 | package = AC008E422D7A4C8900CBC247 /* XCRemoteSwiftPackageReference "microsoft-authentication-library-for-objc" */;
500 | productName = MSAL;
501 | };
502 | AC7BDDA52D7A745F00C46F2D /* IntuneMAMSwift */ = {
503 | isa = XCSwiftPackageProductDependency;
504 | package = AC7BDDA42D7A745F00C46F2D /* XCRemoteSwiftPackageReference "ms-intune-app-sdk-ios" */;
505 | productName = IntuneMAMSwift;
506 | };
507 | AC7BDDA72D7A745F00C46F2D /* IntuneMAMTelemetry */ = {
508 | isa = XCSwiftPackageProductDependency;
509 | package = AC7BDDA42D7A745F00C46F2D /* XCRemoteSwiftPackageReference "ms-intune-app-sdk-ios" */;
510 | productName = IntuneMAMTelemetry;
511 | };
512 | /* End XCSwiftPackageProductDependency section */
513 | };
514 | rootObject = C01A67D420DAB03F00F80AFB /* Project object */;
515 | }
516 |
--------------------------------------------------------------------------------
/chatr.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/chatr.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/chatr.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "originHash" : "041cf42cc35dd3b9b3847f9285240426c190666d2897e1e018ed53f1701920a8",
3 | "pins" : [
4 | {
5 | "identity" : "microsoft-authentication-library-for-objc",
6 | "kind" : "remoteSourceControl",
7 | "location" : "https://github.com/AzureAD/microsoft-authentication-library-for-objc.git",
8 | "state" : {
9 | "revision" : "be848ee7fa9516cec47ae6de47cf1087d51bc774",
10 | "version" : "1.9.0"
11 | }
12 | },
13 | {
14 | "identity" : "ms-intune-app-sdk-ios",
15 | "kind" : "remoteSourceControl",
16 | "location" : "https://github.com/microsoftconnect/ms-intune-app-sdk-ios.git",
17 | "state" : {
18 | "revision" : "8be6896791fd45228a6906f9b8608418b96ab24f",
19 | "version" : "20.5.0"
20 | }
21 | }
22 | ],
23 | "version" : 3
24 | }
25 |
--------------------------------------------------------------------------------
/chatr.xcodeproj/xcshareddata/xcschemes/chatr.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
37 |
38 |
39 |
40 |
41 |
42 |
52 |
54 |
60 |
61 |
62 |
63 |
69 |
71 |
77 |
78 |
79 |
80 |
82 |
83 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/chatr/AboutUsPage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) Microsoft Corporation. All rights reserved.
3 | //
4 |
5 | import IntuneMAMSwift
6 |
7 | class AboutUsPage: UIViewController, UITextViewDelegate, IntuneMAMPolicyDelegate{
8 |
9 | // variables used throughout this class
10 | @IBOutlet weak var aboutUsText: UITextView!
11 | @IBOutlet weak var backButton: UIButton!
12 |
13 | override func viewDidLoad() {
14 | super.viewDidLoad()
15 |
16 | // set up the link and text that will be displayed on the About us page
17 | let developerGuide: [NSAttributedString.Key : Any] = [.link: NSURL(string: "https://docs.microsoft.com/intune/app-sdk-ios")!, .foregroundColor: UIColor.blue]
18 | let text = NSMutableAttributedString(string: "Chatr was built to demonstrate the integration of line-of-business apps with Microsoft Intune's iOS MAM SDK. Chatr is a messaging application that allows users to save and print their conversation transcript. \n \nMore information about the SDK is available here. \n \nCurrent SDK Version: \(IntuneMAMVersionInfo.sdkVersion())")
19 | text.setAttributes(developerGuide, range: NSMakeRange(256, 4))
20 |
21 | // assign the link and text to the page
22 | self.aboutUsText.delegate = self
23 | self.aboutUsText.attributedText = text
24 | self.aboutUsText.isUserInteractionEnabled = true
25 | self.aboutUsText.isEditable = false
26 | self.aboutUsText.layer.cornerRadius = 10
27 | self.backButton.setImage(#imageLiteral(resourceName: "backarrow.png"), for: UIControl.State.normal)
28 | self.backButton.imageView!.contentMode = .scaleAspectFit
29 | self.backButton.addTarget(self, action: #selector (self.dismiss), for: .touchUpInside)
30 | }
31 |
32 | //Button action triggered when back button is pressed on the "About Us" page.
33 | //Dismisses the view.
34 | @IBAction func dismiss(_ sender: UIButton) {
35 | self.dismiss(animated: true, completion: nil)
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/chatr/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) Microsoft Corporation. All rights reserved.
3 | //
4 |
5 | import IntuneMAMSwift
6 |
7 | @UIApplicationMain
8 | class AppDelegate: UIResponder, UIApplicationDelegate {
9 |
10 | let enrollmentDelegate = EnrollmentDelegateClass.init()
11 | let policyDelegate = PolicyDelegateClass.init()
12 | var window: UIWindow?
13 |
14 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
15 | // Override point for customization after application launch.
16 |
17 | let storyboard: UIStoryboard = UIStoryboard(name:"Main", bundle: Bundle.main)
18 |
19 | //check for enrolled account
20 | let currentUser = IntuneMAMEnrollmentManager.instance().enrolledAccountId()
21 | if nil != currentUser && !currentUser!.isEmpty {
22 | //if an account is enrolled, skip over login page to main page
23 | //Do this by setting the main chat page to the rootViewController
24 | let mainPage = storyboard.instantiateViewController(withIdentifier: "ChatPage")
25 | self.window?.rootViewController = mainPage
26 | } else{
27 | //if not logged in, set the login page to the rootViewController
28 | let loginPage = storyboard.instantiateViewController(withIdentifier: "LoginPage")
29 | self.window?.rootViewController = loginPage
30 | }
31 |
32 | return true
33 | }
34 |
35 | func applicationWillResignActive(_ application: UIApplication) {
36 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
37 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
38 | }
39 |
40 | func applicationDidEnterBackground(_ application: UIApplication) {
41 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
42 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
43 | }
44 |
45 | func applicationWillEnterForeground(_ application: UIApplication) {
46 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
47 | }
48 |
49 | func applicationDidBecomeActive(_ application: UIApplication) {
50 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
51 | }
52 |
53 | func applicationWillTerminate(_ application: UIApplication) {
54 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
55 | }
56 |
57 | func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
58 |
59 | //Set the delegate of the IntuneMAMPolicyManager to an instance of the PolicyDelegateClass
60 | IntuneMAMPolicyManager.instance().delegate = self.policyDelegate
61 |
62 | //Set the delegate of the IntuneMAMEnrollmentManager to an instance of the EnrollmentDelegateClass
63 | IntuneMAMEnrollmentManager.instance().delegate = self.enrollmentDelegate
64 |
65 | return true
66 | }
67 |
68 | }
69 |
70 |
--------------------------------------------------------------------------------
/chatr/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "20x20",
5 | "idiom" : "iphone",
6 | "filename" : "Icon-App-20x20@2x.png",
7 | "scale" : "2x"
8 | },
9 | {
10 | "size" : "20x20",
11 | "idiom" : "iphone",
12 | "filename" : "Icon-App-20x20@3x.png",
13 | "scale" : "3x"
14 | },
15 | {
16 | "size" : "29x29",
17 | "idiom" : "iphone",
18 | "filename" : "Icon-App-29x29@2x.png",
19 | "scale" : "2x"
20 | },
21 | {
22 | "size" : "29x29",
23 | "idiom" : "iphone",
24 | "filename" : "Icon-App-29x29@3x.png",
25 | "scale" : "3x"
26 | },
27 | {
28 | "size" : "40x40",
29 | "idiom" : "iphone",
30 | "filename" : "Icon-App-40x40@2x.png",
31 | "scale" : "2x"
32 | },
33 | {
34 | "size" : "40x40",
35 | "idiom" : "iphone",
36 | "filename" : "Icon-App-40x40@3x.png",
37 | "scale" : "3x"
38 | },
39 | {
40 | "size" : "60x60",
41 | "idiom" : "iphone",
42 | "filename" : "Icon-App-60x60@2x.png",
43 | "scale" : "2x"
44 | },
45 | {
46 | "size" : "60x60",
47 | "idiom" : "iphone",
48 | "filename" : "Icon-App-60x60@3x.png",
49 | "scale" : "3x"
50 | },
51 | {
52 | "size" : "20x20",
53 | "idiom" : "ipad",
54 | "filename" : "Icon-App-20x20@1x.png",
55 | "scale" : "1x"
56 | },
57 | {
58 | "size" : "20x20",
59 | "idiom" : "ipad",
60 | "filename" : "Icon-App-20x20@2x-1.png",
61 | "scale" : "2x"
62 | },
63 | {
64 | "size" : "29x29",
65 | "idiom" : "ipad",
66 | "filename" : "Icon-App-29x29@1x.png",
67 | "scale" : "1x"
68 | },
69 | {
70 | "size" : "29x29",
71 | "idiom" : "ipad",
72 | "filename" : "Icon-App-29x29@2x-1.png",
73 | "scale" : "2x"
74 | },
75 | {
76 | "size" : "40x40",
77 | "idiom" : "ipad",
78 | "filename" : "Icon-App-40x40@1x.png",
79 | "scale" : "1x"
80 | },
81 | {
82 | "size" : "40x40",
83 | "idiom" : "ipad",
84 | "filename" : "Icon-App-40x40@2x-1.png",
85 | "scale" : "2x"
86 | },
87 | {
88 | "size" : "76x76",
89 | "idiom" : "ipad",
90 | "filename" : "Icon-App-76x76@1x.png",
91 | "scale" : "1x"
92 | },
93 | {
94 | "size" : "76x76",
95 | "idiom" : "ipad",
96 | "filename" : "Icon-App-76x76@2x.png",
97 | "scale" : "2x"
98 | },
99 | {
100 | "size" : "83.5x83.5",
101 | "idiom" : "ipad",
102 | "filename" : "Icon-App-83.5x83.5@2x.png",
103 | "scale" : "2x"
104 | },
105 | {
106 | "size" : "1024x1024",
107 | "idiom" : "ios-marketing",
108 | "filename" : "DM (2).jpg",
109 | "scale" : "1x"
110 | }
111 | ],
112 | "info" : {
113 | "version" : 1,
114 | "author" : "xcode"
115 | }
116 | }
--------------------------------------------------------------------------------
/chatr/Assets.xcassets/AppIcon.appiconset/DM (2).jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/AppIcon.appiconset/DM (2).jpg
--------------------------------------------------------------------------------
/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
--------------------------------------------------------------------------------
/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x-1.png
--------------------------------------------------------------------------------
/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
--------------------------------------------------------------------------------
/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
--------------------------------------------------------------------------------
/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
--------------------------------------------------------------------------------
/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x-1.png
--------------------------------------------------------------------------------
/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
--------------------------------------------------------------------------------
/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
--------------------------------------------------------------------------------
/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
--------------------------------------------------------------------------------
/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x-1.png
--------------------------------------------------------------------------------
/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
--------------------------------------------------------------------------------
/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
--------------------------------------------------------------------------------
/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
--------------------------------------------------------------------------------
/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
--------------------------------------------------------------------------------
/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
--------------------------------------------------------------------------------
/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
--------------------------------------------------------------------------------
/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
--------------------------------------------------------------------------------
/chatr/Assets.xcassets/ChatPage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) Microsoft Corporation. All rights reserved.
3 | //
4 | // Code for sidebar implementation adopted from: https://youtu.be/GOSIz7JbZMA by Yogesh Patel
5 | // Code for alert message adopted from: https://www.simplifiedios.net/ios-show-alert-using-uialertcontroller/ by Belal Khan
6 | // Code for print adopted from: https://stackoverflow.com/questions/32403634/airprint-contents-of-a-uiview answer by Jody Heavener
7 | // Other inspiration: https://stackoverflow.com/questions/31870206/how-to-insert-new-cell-into-uitableview-in-swift responses by EI Captain v2.0 and Dharmesh Kheni
8 | //
9 | //
10 |
11 | import UIKit
12 |
13 | // global variable used for saving conversations
14 | var conversation:[(sender:String, message:NSAttributedString)] = []
15 | let savedConvo = UserDefaults.init()
16 |
17 | let appDelegate = UIApplication.shared.delegate as! AppDelegate
18 |
19 | class ChatPage: UIViewController, UITableViewDelegate, UITableViewDataSource, UITextFieldDelegate {
20 |
21 | // variable used to keep track of whether the send button is already displayed
22 | var alreadyDisplayedSendButton = false
23 |
24 | // variables used for creating the sidebar
25 | @IBOutlet weak var sideBarTable: UITableView!
26 | var isMenu:Bool = false // variable that indicates if the menu is being displayed
27 | let sideBarFeatures = ["Save","Print", "About us","Settings", "Log out"] // the options on the sidebar
28 | let sideBarImg = [#imageLiteral(resourceName: "save"),#imageLiteral(resourceName: "print"),#imageLiteral(resourceName: "information"),#imageLiteral(resourceName: "settings"),#imageLiteral(resourceName: "profile")] // images for the sidebar options
29 |
30 | // variables used for chat
31 | @IBOutlet weak var typedChatView: UITextView!
32 | @IBOutlet weak var chatTable: UITableView!
33 |
34 | // variables used for printing
35 | @IBOutlet var wholePageView: UIView!
36 |
37 | // variable to display group name on top of the chat page, default set to 'Chatr'
38 | // Onpage load: updated to be user's group name based on targeted app config
39 | @IBOutlet weak var groupName: UITextField!
40 |
41 | // variable to move textfield for keyboard actions
42 | @IBOutlet weak var keyboardHeightLayoutConstraint: NSLayoutConstraint!
43 |
44 | //variable to reference the position and dimensions of the top bar
45 | @IBOutlet weak var topBarView: UIView!
46 |
47 | //variable to store initial save by policy permissions
48 | var isSaveAllowed = Bool()
49 |
50 | //override the ChatPage View Controller initializer
51 | required init? (coder aDecoder: NSCoder) {
52 |
53 | super.init(coder: aDecoder)
54 |
55 | //register for the IntuneMAMAppConfigDidChange notification
56 | NotificationCenter.default.addObserver(self,
57 | selector: #selector(onIntuneMAMAppConfigDidChange),
58 | name: NSNotification.Name.IntuneMAMAppConfigDidChange,
59 | object: IntuneMAMAppConfigManager.instance())
60 |
61 | //register for the IntuneMAMPolicyDidChange notification
62 | NotificationCenter.default.addObserver(self,
63 | selector: #selector(onIntuneMAMPolicyDidChange),
64 | name: NSNotification.Name.IntuneMAMPolicyDidChange,
65 | object: IntuneMAMPolicyManager.instance())
66 | }
67 |
68 | @objc func onIntuneMAMAppConfigDidChange() {
69 | //query the app config and update the user name on the top of the chat page
70 | self.groupName.text = ObjCUtils.getUserGroupName()
71 | }
72 |
73 | @objc func onIntuneMAMPolicyDidChange() {
74 | if ObjCUtils.isSaveToLocalDriveAllowed() {
75 | saveAllowedByPolicy()
76 | }
77 | else {
78 | saveNotAllowedByPolicy()
79 | }
80 | }
81 |
82 | /*!
83 | Button action triggered when send button is pressed on chat page
84 |
85 | Empties out the text field, creates a new view with the message and triggers a response.
86 | */
87 | @IBAction func sendChat(_ sender: UIButton) {
88 |
89 | let align = NSMutableParagraphStyle()
90 | align.alignment = .right
91 |
92 | let fromMessage = NSMutableAttributedString(string: typedChatView.text!, attributes: [.paragraphStyle: align])
93 | typedChatView.text = ""
94 | conversation.append((sender: "from", message: fromMessage))
95 |
96 | // update the message board to include the update
97 | self.chatTable.beginUpdates()
98 | self.chatTable.insertRows(at: [IndexPath.init(row: conversation.count-1, section: 0)], with: .automatic)
99 | self.chatTable.endUpdates()
100 |
101 | // send the reply
102 | replyChat()
103 | }
104 |
105 | /*!
106 | Creates a response message with links to the documentation and adds it to the chat view.
107 | Called each time a message is sent.
108 | */
109 | func replyChat() {
110 |
111 | //create reply message
112 | let developerGuide: [NSAttributedStringKey : Any] = [.link: NSURL(string: "https://docs.microsoft.com/en-us/intune/app-sdk-ios")!, .foregroundColor: UIColor.blue]
113 | let faqPage: [NSAttributedStringKey : Any] = [.link : NSURL(string: "https://docs.microsoft.com/en-us/intune/app-sdk-ios#faqs")!, .foregroundColor: UIColor.blue]
114 |
115 | let align = NSMutableParagraphStyle()
116 | align.alignment = .left
117 |
118 | let replyMessage = NSMutableAttributedString(string: "Please refer to our documentation and read our faq page for any outstanding concerns. \nThank you.", attributes: [.paragraphStyle: align])
119 | replyMessage.setAttributes(developerGuide, range: NSMakeRange(20, 13))
120 | replyMessage.setAttributes(faqPage, range: NSMakeRange(47, 3))
121 |
122 | // add it to the conversation list
123 | conversation.append((sender: "to", message: replyMessage))
124 |
125 | //update the message board to include the reply
126 | self.chatTable.beginUpdates()
127 | self.chatTable.insertRows(at: [IndexPath.init(row: conversation.count-1, section: 0)], with: .automatic)
128 | self.chatTable.endUpdates()
129 | }
130 |
131 | override func viewDidLoad() {
132 | super.viewDidLoad()
133 |
134 | //prevent the display of empty cells at the bottom of the sidebar menu by adding a zero height table footer view
135 | sideBarTable.tableFooterView = UIView(frame: CGRect(x:0, y:0, width: 0, height: 0))
136 | sideBarTable.tableFooterView?.isHidden = true
137 | sideBarTable.backgroundColor = UIColor.clear
138 |
139 | //ensures self-sizing sidebar table view cells
140 | //the sidebar table view will use Auto Layout constraints and the cell's contents to determine each cell's height
141 | sideBarTable.estimatedRowHeight = 40
142 | sideBarTable.rowHeight = UITableViewAutomaticDimension
143 |
144 | // when the view is loaded, hide the sidebar table
145 | sideBarTable.isHidden = true
146 | isMenu = false
147 |
148 | // round the corners of the chat view
149 | typedChatView.layer.cornerRadius = 10
150 |
151 | // change user's group name on top of the chat page to the initial group name
152 | self.groupName.text = ObjCUtils.getUserGroupName()
153 |
154 | NotificationCenter.default.addObserver(self,
155 | selector: #selector(self.keyboardNotification(notification:)),
156 | name: NSNotification.Name.UIKeyboardWillChangeFrame,
157 | object: nil)
158 |
159 | }
160 |
161 | //programmatically create send button after Auto Layout lays out the main view and subviews
162 | override func viewDidLayoutSubviews() {
163 | //ensures a new send button is not added every time a message is sent in the chat
164 | if !alreadyDisplayedSendButton {
165 |
166 | let sendButton = UIButton(frame: CGRect(x: typedChatView.frame.origin.x + typedChatView.frame.width + 4, y: typedChatView.frame.origin.y - 4, width: 57, height: 39))
167 | sendButton.backgroundColor = .clear
168 | sendButton.setTitle("SEND", for: .normal)
169 | sendButton.addTarget(self, action: #selector (sendChat), for: .touchUpInside)
170 |
171 | self.view.addSubview(sendButton)
172 |
173 | alreadyDisplayedSendButton = true
174 | }
175 | }
176 |
177 | deinit {
178 | NotificationCenter.default.removeObserver(self)
179 | }
180 |
181 | @objc func keyboardNotification(notification: NSNotification) {
182 | if let userInfo = notification.userInfo {
183 | let endFrame = (userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue
184 | let endFrameY = endFrame?.origin.y ?? 0
185 | let duration:TimeInterval = (userInfo[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue ?? 0
186 | let animationCurveRawNSN = userInfo[UIKeyboardAnimationCurveUserInfoKey] as? NSNumber
187 | let animationCurveRaw = animationCurveRawNSN?.uintValue ?? UIViewAnimationOptions.curveEaseInOut.rawValue
188 | let animationCurve:UIViewAnimationOptions = UIViewAnimationOptions(rawValue: animationCurveRaw)
189 | if endFrameY >= UIScreen.main.bounds.size.height {
190 | self.keyboardHeightLayoutConstraint?.constant = 10.0
191 | } else {
192 | self.keyboardHeightLayoutConstraint?.constant = (endFrame?.size.height)! + 10.0
193 | }
194 | UIView.animate(withDuration: duration,
195 | delay: TimeInterval(0),
196 | options: animationCurve,
197 | animations: { self.view.layoutIfNeeded() },
198 | completion: nil)
199 | }
200 | }
201 |
202 | //hide the keyboard when the user taps the return key
203 | func textView(textView: UITextView, shouldChangeTextInRange range: NSRange, replacementText text: String) -> Bool {
204 | if (text == "\n") {
205 | textView.resignFirstResponder()
206 | }
207 | return true
208 | }
209 |
210 |
211 | override func didReceiveMemoryWarning() {
212 | super.didReceiveMemoryWarning()
213 | // Dispose of any resources that can be recreated.
214 | }
215 |
216 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
217 | if tableView == sideBarTable { // this is the sideBar table
218 | return sideBarFeatures.count
219 | } else { // this is the conversations table
220 | return conversation.count
221 | }
222 | }
223 |
224 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
225 | if tableView == sideBarTable {
226 | let cell:SideBarTableCell = tableView.dequeueReusableCell(withIdentifier: "sideCell") as! SideBarTableCell
227 | cell.cellImg.image = sideBarImg[indexPath.row]
228 | cell.cellLbl.text = sideBarFeatures[indexPath.row]
229 | return cell
230 | } else {
231 | let sendMessageCell:chatTableViewCell = chatTable.dequeueReusableCell(withIdentifier: "chatCell") as! chatTableViewCell
232 |
233 | sendMessageCell.messageView.attributedText = conversation[indexPath.row].message
234 |
235 | return sendMessageCell
236 | }
237 | }
238 |
239 | /*
240 | Button action triggered when the the Chatr logo button on the top left is clicked
241 |
242 | Reveals/hides the sideBar menu when the button is pressed depending on the state
243 | - If the menu was hidden when the button is pressed, then it will reveal the menu and vise versa.
244 | */
245 | @IBAction func sideBarMenu(_ sender: Any) {
246 | sideBarTable.isHidden = false
247 |
248 | if !isMenu {
249 | // reveal sideBar menu
250 | isMenu = true
251 | sideBarTable.frame = CGRect(x: 0, y: topBarView.frame.height + topBarView.frame.origin.y, width: 0, height: 341)
252 | UIView.setAnimationDuration(0.15)
253 | UIView.setAnimationDelegate(self)
254 | UIView.beginAnimations("sideBarAnimation", context: nil)
255 | sideBarTable.frame = CGRect(x: 0, y: topBarView.frame.height + topBarView.frame.origin.y, width: 176, height: 341)
256 | UIView.commitAnimations()
257 | }
258 | else {
259 | hideSideBarMenu()
260 | }
261 | }
262 |
263 | /*!
264 | Hides the sidebar menu
265 | */
266 | func hideSideBarMenu() {
267 | // hide sideBar menu
268 | sideBarTable.isHidden = true
269 | isMenu = false
270 | sideBarTable.frame = CGRect(x: 0, y: topBarView.frame.height + topBarView.frame.origin.y, width: 176, height: 341)
271 | UIView.setAnimationDuration(0.15)
272 | UIView.setAnimationDelegate(self)
273 | UIView.beginAnimations("sideBarAnimation", context: nil)
274 | sideBarTable.frame = CGRect(x: 0, y: topBarView.frame.height + topBarView.frame.origin.y, width: 0, height: 341)
275 | UIView.commitAnimations()
276 | }
277 |
278 | //Enumeration that defines options within the side bar menu
279 | enum SideBarOptions: Int{
280 | case save = 0
281 | case print
282 | case aboutUs
283 | case settings
284 | case logout
285 | }
286 |
287 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
288 | if tableView == sideBarTable {
289 | let sideBarOption = SideBarOptions(rawValue: indexPath.row)
290 | // Complete an action based on the item pressed on the sidebar
291 | switch sideBarOption {
292 | case .save?:
293 | // Check if save is allowed by policy
294 | if ObjCUtils.isSaveToLocalDriveAllowed() {
295 | saveAllowedByPolicy()
296 | }
297 | else {
298 | saveNotAllowedByPolicy()
299 | }
300 | case .print?:
301 | //Print the conversation
302 | printConvo()
303 | case .aboutUs?:
304 | //Display the about us page
305 | let aboutUs:AboutUsPage = self.storyboard?.instantiateViewController(withIdentifier: "aboutPage") as! AboutUsPage
306 | present(aboutUs, animated:true, completion: nil)
307 | case .settings?:
308 | //Display the settings page
309 | let settings:SettingsPage = self.storyboard?.instantiateViewController(withIdentifier: "settingsPage") as! SettingsPage
310 | present(settings, animated:true, completion: nil)
311 | case .logout?:
312 | //Log out user
313 | ObjCUtils.logout()
314 | default: break
315 | }
316 | }
317 | }
318 |
319 | func saveAllowedByPolicy() {
320 | savedConvo.set(conversation, forKey: "savedConversation ")
321 | //Alert the user that saving is enabled
322 | let alert = UIAlertController(title: "Conversation Saved",
323 | message: "Your conversation has been successfully saved to your device.",
324 | preferredStyle: .alert)
325 | let closeAlert = UIAlertAction(title: "Ok",
326 | style: .default,
327 | handler: nil)
328 | alert.addAction(closeAlert)
329 | present(alert, animated: true, completion: nil)
330 | }
331 |
332 | func saveNotAllowedByPolicy() {
333 | // Alert the user that saving is disabled
334 | let alert = UIAlertController(title: "Save Disabled",
335 | message: "Saving conversations to local storage has been disabled by your IT admin.",
336 | preferredStyle: .alert)
337 | let closeAlert = UIAlertAction(title: "Ok",
338 | style: .default,
339 | handler: nil)
340 | alert.addAction(closeAlert)
341 | present(alert, animated: true, completion: nil)
342 | }
343 |
344 | /*!
345 | Presents the user with a print preview of the chat page.
346 | Called by the side bar table
347 | */
348 | func printConvo() {
349 | hideSideBarMenu() // hide the side bar before you move on
350 |
351 | let printInfo = UIPrintInfo(dictionary:nil)
352 | printInfo.outputType = UIPrintInfoOutputType.general
353 | printInfo.jobName = "Print Chat Page"
354 |
355 | let printController = UIPrintInteractionController.shared
356 | printController.printInfo = printInfo
357 | printController.printingItem = wholePageView.toImage()
358 | printController.present(animated: true, completionHandler: nil)
359 | }
360 | }
361 |
362 | extension UIView {
363 | // converts a UIView to an image
364 | // used in the print() function for ChatPage
365 | func toImage() -> UIImage {
366 | // code logic modified from answer by Jody Heavener @ https://stackoverflow.com/questions/32403634/airprint-contents-of-a-uiview
367 | UIGraphicsBeginImageContextWithOptions(bounds.size, false, UIScreen.main.scale)
368 |
369 | drawHierarchy(in: self.bounds, afterScreenUpdates: true)
370 |
371 | let image = UIGraphicsGetImageFromCurrentImageContext()
372 | UIGraphicsEndImageContext()
373 | return image!
374 | }
375 | }
376 |
--------------------------------------------------------------------------------
/chatr/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/chatr/Assets.xcassets/backarrow.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "backarrow.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/chatr/Assets.xcassets/backarrow.imageset/backarrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/backarrow.imageset/backarrow.png
--------------------------------------------------------------------------------
/chatr/Assets.xcassets/information.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "Picture1.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/chatr/Assets.xcassets/information.imageset/Picture1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/information.imageset/Picture1.png
--------------------------------------------------------------------------------
/chatr/Assets.xcassets/logo.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "logo.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "iTunesArtwork@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "iTunesArtwork@3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
--------------------------------------------------------------------------------
/chatr/Assets.xcassets/logo.imageset/iTunesArtwork@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/logo.imageset/iTunesArtwork@2x.png
--------------------------------------------------------------------------------
/chatr/Assets.xcassets/logo.imageset/iTunesArtwork@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/logo.imageset/iTunesArtwork@3x.png
--------------------------------------------------------------------------------
/chatr/Assets.xcassets/logo.imageset/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/logo.imageset/logo.png
--------------------------------------------------------------------------------
/chatr/Assets.xcassets/menubar.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "DM-3.jpg",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/chatr/Assets.xcassets/menubar.imageset/DM-3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/menubar.imageset/DM-3.jpg
--------------------------------------------------------------------------------
/chatr/Assets.xcassets/print.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "print - graphic.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/chatr/Assets.xcassets/print.imageset/print - graphic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/print.imageset/print - graphic.png
--------------------------------------------------------------------------------
/chatr/Assets.xcassets/profile.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "person - graphic.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/chatr/Assets.xcassets/profile.imageset/person - graphic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/profile.imageset/person - graphic.png
--------------------------------------------------------------------------------
/chatr/Assets.xcassets/save.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "save - graphic.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/chatr/Assets.xcassets/save.imageset/save - graphic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/save.imageset/save - graphic.png
--------------------------------------------------------------------------------
/chatr/Assets.xcassets/settings.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "settings-graphic.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/chatr/Assets.xcassets/settings.imageset/settings-graphic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/settings.imageset/settings-graphic.png
--------------------------------------------------------------------------------
/chatr/Assets.xcassets/sidebar.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "resizing" : {
5 | "mode" : "9-part",
6 | "center" : {
7 | "mode" : "tile",
8 | "width" : 1,
9 | "height" : 1
10 | },
11 | "cap-insets" : {
12 | "bottom" : 240,
13 | "top" : 257,
14 | "right" : 357,
15 | "left" : 142
16 | }
17 | },
18 | "idiom" : "universal",
19 | "filename" : "DM-3.jpg",
20 | "scale" : "1x"
21 | },
22 | {
23 | "idiom" : "universal",
24 | "scale" : "2x"
25 | },
26 | {
27 | "idiom" : "universal",
28 | "scale" : "3x"
29 | }
30 | ],
31 | "info" : {
32 | "version" : 1,
33 | "author" : "xcode"
34 | }
35 | }
--------------------------------------------------------------------------------
/chatr/Assets.xcassets/sidebar.imageset/DM-3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/chatr/Assets.xcassets/sidebar.imageset/DM-3.jpg
--------------------------------------------------------------------------------
/chatr/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/chatr/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
31 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
99 |
100 |
101 |
102 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 |
345 |
346 |
354 |
355 |
356 |
357 |
358 |
359 |
360 |
361 |
362 |
371 |
372 |
373 |
374 |
375 |
376 |
377 |
378 |
379 |
380 |
381 |
382 |
383 |
384 |
385 |
386 |
387 |
388 |
389 |
390 |
391 |
392 |
393 |
394 |
395 |
396 |
397 |
398 |
399 |
400 |
401 |
402 |
403 |
404 |
405 |
406 |
407 |
408 |
409 |
--------------------------------------------------------------------------------
/chatr/ChatPage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) Microsoft Corporation. All rights reserved.
3 | //
4 |
5 | import IntuneMAMSwift
6 |
7 | // global variable used for saving conversations
8 | var conversation:[(sender:String, message:NSAttributedString)] = []
9 |
10 | class ChatPage: UIViewController, UITableViewDelegate, UITableViewDataSource {
11 |
12 | // variable used to keep track of whether the send button is already displayed
13 | var alreadyDisplayedSendButton = false
14 |
15 | //variable for the currently enrolled user
16 | var currentUser: String!
17 |
18 | // variables used for creating the sidebar
19 | @IBOutlet weak var sideBarTable: UITableView!
20 | var isMenu:Bool = false // variable that indicates if the menu is being displayed
21 | let sideBarFeatures = ["Save","Print", "About us","Settings", "Log out"] // the options on the sidebar
22 | let sideBarImg = [#imageLiteral(resourceName: "save"),#imageLiteral(resourceName: "print"),#imageLiteral(resourceName: "information"),#imageLiteral(resourceName: "settings"),#imageLiteral(resourceName: "profile")] // images for the sidebar options
23 | var menuWidthConstraint: NSLayoutConstraint! // Contraint to animate the menu
24 |
25 | // variables used for chat
26 | @IBOutlet weak var typedChatView: UITextView!
27 | @IBOutlet weak var chatTable: UITableView!
28 |
29 | // variables used for printing
30 | @IBOutlet var wholePageView: UIView!
31 |
32 | // variable to display group name on top of the chat page, default set to 'Chatr'
33 | // Onpage load: updated to be user's group name based on targeted app config
34 | @IBOutlet weak var groupName: UITextField!
35 |
36 | // variable to move textfield for keyboard actions
37 | @IBOutlet weak var keyboardHeightLayoutConstraint: NSLayoutConstraint!
38 |
39 | //variable to reference the position and dimensions of the top bar
40 | @IBOutlet weak var topBarView: UIView!
41 |
42 | //variable to store initial save by policy permissions
43 | var isSaveAllowed = Bool()
44 |
45 | //override the ChatPage View Controller initializer
46 | required init? (coder aDecoder: NSCoder) {
47 |
48 | super.init(coder: aDecoder)
49 |
50 | //register for the IntuneMAMAppConfigDidChange notification
51 | NotificationCenter.default.addObserver(self,
52 | selector: #selector(onIntuneMAMAppConfigDidChange),
53 | name: NSNotification.Name.IntuneMAMAppConfigDidChange,
54 | object: IntuneMAMAppConfigManager.instance())
55 |
56 | //register for the IntuneMAMPolicyDidChange notification
57 | NotificationCenter.default.addObserver(self,
58 | selector: #selector(onIntuneMAMPolicyDidChange),
59 | name: NSNotification.Name.IntuneMAMPolicyDidChange,
60 | object: IntuneMAMPolicyManager.instance())
61 |
62 | //Get the current user
63 | self.currentUser = IntuneMAMEnrollmentManager.instance().enrolledAccountId()!
64 |
65 | //query the app policy and update the initial save-as policy permissions
66 | self.isSaveAllowed = self.getSaveStatus()
67 | }
68 |
69 | @objc func onIntuneMAMAppConfigDidChange() {
70 | //query the app config and update the user name on the top of the chat page
71 | self.groupName.text = self.getUserGroupName()
72 | }
73 |
74 | @objc func onIntuneMAMPolicyDidChange() {
75 | self.isSaveAllowed = self.getSaveStatus()
76 | }
77 |
78 | override func viewDidLoad() {
79 | super.viewDidLoad()
80 |
81 | //prevent the display of empty cells at the bottom of the sidebar menu by adding a zero height table footer view
82 | self.sideBarTable.tableFooterView = UIView(frame: CGRect(x:0, y:0, width: 0, height: 0))
83 | self.sideBarTable.tableFooterView?.isHidden = true
84 | self.sideBarTable.backgroundColor = UIColor.clear
85 |
86 | //ensures self-sizing sidebar table view cells
87 | //the sidebar table view will use Auto Layout constraints and the cell's contents to determine each cell's height
88 | self.sideBarTable.estimatedRowHeight = 40
89 | self.sideBarTable.rowHeight = UITableView.automaticDimension
90 |
91 | // when the view is loaded, hide the sidebar table
92 | self.sideBarTable.isHidden = true
93 | self.isMenu = false
94 |
95 | // round the corners of the chat view
96 | self.typedChatView.layer.cornerRadius = 10
97 |
98 | // change user's group name on top of the chat page, one of the app config settings
99 | self.groupName.text = self.getUserGroupName()
100 |
101 | NotificationCenter.default.addObserver(self,
102 | selector: #selector(self.keyboardNotification(notification:)),
103 | name: UIResponder.keyboardWillChangeFrameNotification,
104 | object: nil)
105 | //Check the keychain for chat messages and drafted messages to load into the view
106 | if let messageArray: [String] = KeychainManager.getSentMessages(forUser: self.currentUser){
107 | //If messages are present, populate the screen with them
108 | self.populateChatScreen(messageArray: messageArray)
109 | }
110 | let draftMessage: String? = KeychainManager.getDraftedMessage(forUser: self.currentUser)
111 | if nil != draftMessage{
112 | //If a draft message is present, add it to the message entry bar
113 | self.typedChatView.text = draftMessage!
114 | }
115 |
116 | //Add an observer to save any drafted message when the app terminates
117 | NotificationCenter.default.addObserver(self, selector: #selector(self.saveDraftedMessage), name: UIApplication.willResignActiveNotification, object: nil)
118 | }
119 |
120 | //programmatically create send button after Auto Layout lays out the main view and subviews
121 | override func viewDidLayoutSubviews() {
122 | //ensures a new send button is not added every time a message is sent in the chat
123 | if !self.alreadyDisplayedSendButton {
124 |
125 | let sendButton = UIButton(frame: CGRect(x: self.typedChatView.frame.origin.x + self.typedChatView.frame.width + 4, y: self.typedChatView.frame.origin.y - 4, width: 57, height: 39))
126 | sendButton.backgroundColor = .clear
127 | sendButton.setTitle("SEND", for: .normal)
128 | sendButton.addTarget(self, action: #selector (self.sendChat), for: .touchUpInside)
129 |
130 | self.view.addSubview(sendButton)
131 | sendButton.translatesAutoresizingMaskIntoConstraints = false
132 | sendButton.bottomAnchor.constraint(equalTo: self.typedChatView.bottomAnchor).isActive = true
133 | sendButton.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -8).isActive = true
134 |
135 | self.alreadyDisplayedSendButton = true
136 | }
137 | }
138 |
139 | /*!
140 | Button action triggered when send button is pressed on chat page
141 |
142 | Empties out the text field, creates a new view with the message and triggers a response.
143 |
144 | This function also stores the message in the keychain using the KeychainManager
145 | */
146 | @IBAction func sendChat(_ sender: UIButton) {
147 | //First format the string appropriately
148 | let align = NSMutableParagraphStyle()
149 | align.alignment = .right
150 | let fromMessage = NSMutableAttributedString(string: self.typedChatView.text!, attributes: [.paragraphStyle: align])
151 |
152 | //Only take action if there is text in the message
153 | if 0 != fromMessage.length {
154 | //Reset the entry field
155 | self.typedChatView.text = ""
156 | //When any message is sent, delete any draft message in the keychain
157 | _ = KeychainManager.deleteDraftMessage(forUser: self.currentUser)
158 | self.displayChatMessage(message: fromMessage)
159 |
160 | //Add the message to the stored messages in the keychain
161 | KeychainManager.storeSentMessage(sentMessage: fromMessage.string, forUser: self.currentUser)
162 | //Scroll to the bottom of this message
163 | self.scrollToBottom(animated: true)
164 | }
165 | }
166 |
167 | /*
168 | Helper function that takes a formatted message, displays it on the chat page table, and calls replyChat to add a response message.
169 | */
170 | func displayChatMessage(message:NSAttributedString) {
171 | //Add the message to the data source for the chatTable
172 | conversation.append((sender: "from", message: message))
173 |
174 | //Update the message board to include the new message
175 | self.chatTable.beginUpdates()
176 | self.chatTable.insertRows(at: [IndexPath.init(row: conversation.count-1, section: 0)], with: .automatic)
177 | self.chatTable.endUpdates()
178 |
179 | // send the reply
180 | self.replyChat()
181 | }
182 |
183 | /*!
184 | Creates a response message with links to the documentation and adds it to the chat view.
185 | Called each time a message is sent.
186 | */
187 | func replyChat() {
188 |
189 | //create reply message
190 | let developerGuide: [NSAttributedString.Key : Any] = [.link: NSURL(string: "https://docs.microsoft.com/intune/app-sdk-ios")!, .foregroundColor: UIColor.blue]
191 | let faqPage: [NSAttributedString.Key : Any] = [.link : NSURL(string: "https://docs.microsoft.com/intune/app-sdk-ios#faqs")!, .foregroundColor: UIColor.blue]
192 |
193 | let align = NSMutableParagraphStyle()
194 | align.alignment = .left
195 |
196 | let replyMessage = NSMutableAttributedString(string: "Please refer to our documentation and read our faq page for any outstanding concerns. \nThank you.", attributes: [.paragraphStyle: align])
197 | replyMessage.setAttributes(developerGuide, range: NSMakeRange(20, 13))
198 | replyMessage.setAttributes(faqPage, range: NSMakeRange(47, 3))
199 |
200 | // add it to the conversation list
201 | conversation.append((sender: "to", message: replyMessage))
202 |
203 | //update the message board to include the reply
204 | self.chatTable.beginUpdates()
205 | self.chatTable.insertRows(at: [IndexPath.init(row: conversation.count-1, section: 0)], with: .automatic)
206 | self.chatTable.endUpdates()
207 | }
208 |
209 | /*
210 | Function used to clear the chat page messages from the screen
211 | */
212 | func clearChatPage(){
213 | //First clears the array that is a data source for the chat table
214 | conversation.removeAll()
215 | //Then realoads the chat table with the empty data source
216 | self.chatTable.reloadData()
217 | }
218 |
219 | /*
220 | Function used to display a group of messages on the chat page.
221 | @param messageArray: the array of messages to display
222 | */
223 | public func populateChatScreen(messageArray: [String]){
224 | for message in messageArray{
225 | //For every string from the message array, format it and display it
226 | let align = NSMutableParagraphStyle()
227 | align.alignment = .right
228 | let fromMessage = NSMutableAttributedString.init(string: message, attributes: [.paragraphStyle: align])
229 |
230 | self.displayChatMessage(message: fromMessage)
231 | }
232 | }
233 |
234 | /*
235 | Function used to save a drafted message in the message entry field.
236 | If a draft message is present, then it will be saved to the keychain using the KeychainManager class.
237 | */
238 | @objc public func saveDraftedMessage(){
239 | //Save any draft message to the keychain using the KeychainManager class
240 | KeychainManager.storeDraftMessage(draftMessage: typedChatView.text!, forUser: self.currentUser)
241 | }
242 |
243 | //Scrolls to the bottom of the table view
244 | func scrollToBottom(animated: Bool) {
245 | if self.chatTable.numberOfRows(inSection: 0) > 0 {
246 | let index = IndexPath(row: self.chatTable.numberOfRows(inSection: 0)-1, section: 0)
247 | self.chatTable.scrollToRow(at: index, at: .bottom, animated: animated)
248 | }
249 | }
250 |
251 | func getUserGroupName() -> String{
252 | // Get the GroupName value for the user - key value pairing set in the portal
253 | let groupNameKey = "GroupName"
254 | let data = IntuneMAMAppConfigManager.instance().appConfig(forAccountId:self.currentUser)
255 |
256 | // If there are no conflicts for that key, find the value associated with the key
257 | if !data.hasConflict(groupNameKey){
258 | if let groupName = data.stringValue(forKey: groupNameKey, queryType: IntuneMAMStringQueryType.any){
259 | return groupName
260 | }
261 | } else {
262 | // Resolve the conflict by taking the max value
263 | return data.stringValue(forKey: groupNameKey, queryType: IntuneMAMStringQueryType.max)!
264 | }
265 | return "Chatr" // Default, if no GroupName value is set
266 | }
267 |
268 | deinit {
269 | NotificationCenter.default.removeObserver(self)
270 | }
271 |
272 | @objc func keyboardNotification(notification: NSNotification) {
273 | if let userInfo = notification.userInfo {
274 | let endFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue
275 | let endFrameY = endFrame?.origin.y ?? 0
276 | let duration:TimeInterval = (userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue ?? 0
277 | let animationCurveRawNSN = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? NSNumber
278 | let animationCurveRaw = animationCurveRawNSN?.uintValue ?? UIView.AnimationOptions.curveEaseInOut.rawValue
279 | let animationCurve:UIView.AnimationOptions = UIView.AnimationOptions(rawValue: animationCurveRaw)
280 | if endFrameY >= UIScreen.main.bounds.size.height {
281 | self.keyboardHeightLayoutConstraint?.constant = 10.0
282 | } else {
283 | self.keyboardHeightLayoutConstraint?.constant = (endFrame?.size.height)! + 10.0
284 | }
285 | UIView.animate(withDuration: duration,
286 | delay: TimeInterval(0),
287 | options: animationCurve,
288 | animations: { self.view.layoutIfNeeded() },
289 | completion: nil)
290 |
291 | if (endFrame?.size.height)! > 0.0 {
292 | //Scroll to the bottom of the message table if the keyboard is appearing
293 | self.scrollToBottom(animated: true)
294 | }
295 | }
296 | }
297 |
298 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
299 | if tableView == self.sideBarTable { // this is the sideBar table
300 | return self.sideBarFeatures.count
301 | } else { // this is the conversations table
302 | return conversation.count
303 | }
304 | }
305 |
306 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
307 | if tableView == self.sideBarTable {
308 | let cell:SideBarTableCell = tableView.dequeueReusableCell(withIdentifier: "sideCell") as! SideBarTableCell
309 | cell.cellImg.image = sideBarImg[indexPath.row]
310 | cell.cellLbl.text = sideBarFeatures[indexPath.row]
311 | return cell
312 | } else {
313 | let sendMessageCell = self.chatTable.dequeueReusableCell(withIdentifier: "chatCell") as! ChatTableViewCell
314 |
315 | sendMessageCell.messageView.attributedText = conversation[indexPath.row].message
316 |
317 | return sendMessageCell
318 | }
319 | }
320 |
321 | /*
322 | Button action triggered when the the Chatr logo button on the top left is clicked
323 |
324 | Reveals/hides the sideBar menu when the button is pressed depending on the state
325 | - If the menu was hidden when the button is pressed, then it will reveal the menu and vise versa.
326 | */
327 | @IBAction func sideBarMenu(_ sender: Any) {
328 | self.sideBarTable.isHidden = false
329 |
330 | if !isMenu {
331 | // reveal sideBar menu
332 | self.isMenu = true
333 | self.sideBarTable.frame = CGRect(x: 0, y: topBarView.frame.height + topBarView.frame.origin.y, width: 0, height: 341)
334 | self.menuWidthConstraint = self.sideBarTable.widthAnchor.constraint(equalToConstant: 176)
335 | self.sideBarTable.addConstraint(self.menuWidthConstraint)
336 |
337 | UIView.animate(withDuration: 0.15) {
338 | self.sideBarTable.frame = CGRect(x: 0, y: self.topBarView.frame.height + self.topBarView.frame.origin.y, width: 176, height: 341);
339 | }
340 | }
341 | else {
342 | self.hideSideBarMenu()
343 | }
344 | }
345 |
346 | /*!
347 | Hides the sidebar menu
348 | */
349 | func hideSideBarMenu() {
350 | self.isMenu = false
351 | self.sideBarTable.frame = CGRect(x: 0, y: topBarView.frame.height + topBarView.frame.origin.y, width: 176, height: 341)
352 | self.sideBarTable.removeConstraint(self.menuWidthConstraint)
353 |
354 | UIView.animate(withDuration: 0.15, animations: {
355 | self.sideBarTable.frame = CGRect(x: 0, y: self.topBarView.frame.height + self.topBarView.frame.origin.y, width: 0, height: 341);
356 | }) { (_ : Bool) in
357 | self.sideBarTable.isHidden = true
358 | }
359 | }
360 |
361 | //Enumeration that defines options within the side bar menu
362 | enum SideBarOptions: Int{
363 | case save = 0
364 | case print
365 | case aboutUs
366 | case settings
367 | case logout
368 | }
369 |
370 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
371 | if tableView == self.sideBarTable {
372 | self.hideSideBarMenu()
373 | let sideBarOption = SideBarOptions(rawValue: indexPath.row)
374 | // Complete an action based on the item pressed on the sidebar
375 | switch sideBarOption {
376 | case .save?:
377 | // Check if save is allowed by policy
378 | if self.isSaveAllowed {
379 | //Save the conversation and present success alert to user
380 | self.saveConversation(fileName: "savedConversation", conversation: conversation)
381 |
382 | let alert = UIAlertController(title: "Conversation Saved",
383 | message: "Your conversation has been successfully saved to your device.",
384 | preferredStyle: .alert)
385 | let closeAlert = UIAlertAction(title: "OK",
386 | style: .default,
387 | handler: nil)
388 | alert.addAction(closeAlert)
389 | present(alert, animated: true, completion: nil)
390 | }
391 | else {
392 | // Alert the user that saving is disabled
393 | let alert = UIAlertController(title: "Save Disabled",
394 | message: "Saving conversations to local storage has been disabled by your IT admin.",
395 | preferredStyle: .alert)
396 | let closeAlert = UIAlertAction(title: "OK",
397 | style: .default,
398 | handler: nil)
399 | alert.addAction(closeAlert)
400 | present(alert, animated: true, completion: nil)
401 | }
402 | case .print?:
403 | //Check if printing is currently available
404 | //NOTE: While the Intune SDK can prevent printing, this is not the only reason that printing could be unavailable
405 | if UIPrintInteractionController.isPrintingAvailable{
406 | //If printing is available, print the conversation
407 | self.printConvo()
408 | } else {
409 | // Alert the user that saving is unavailable
410 | let alert = UIAlertController(title: "Printing Unavailable",
411 | message: "Printing conversations is currently unavailable on this device.",
412 | preferredStyle: .alert)
413 | let closeAlert = UIAlertAction(title: "OK",
414 | style: .default,
415 | handler: nil)
416 | alert.addAction(closeAlert)
417 | present(alert, animated: true, completion: nil)
418 | }
419 |
420 | case .aboutUs?:
421 | //Display the about us page
422 | let aboutUs:AboutUsPage = self.storyboard?.instantiateViewController(withIdentifier: "aboutPage") as! AboutUsPage
423 | present(aboutUs, animated:true, completion: nil)
424 | case .settings?:
425 | //Display the settings page
426 | let settings:SettingsPage = self.storyboard?.instantiateViewController(withIdentifier: "settingsPage") as! SettingsPage
427 | present(settings, animated:true, completion: nil)
428 | case .logout?:
429 | //To log out user, deregister the user from the SDK and initate a selective wipe of the app
430 | //In the EnrollmentDelegate, the unenrollRequestWithStatus block is executed, and includes logic to wipe tokens on unenrollment
431 | IntuneMAMEnrollmentManager.instance().deRegisterAndUnenrollAccountId(self.currentUser, withWipe: true)
432 | case .none:
433 | return;
434 | }
435 | }
436 | }
437 |
438 | /*!
439 | Presents the user with a print preview of the chat page.
440 | Called by the side bar table
441 | */
442 | func printConvo() {
443 | //Provide basic information about print job
444 | let printInfo = UIPrintInfo(dictionary:nil)
445 | printInfo.outputType = UIPrintInfo.OutputType.general
446 | printInfo.jobName = "Print Chat Page"
447 |
448 | //Initialize a controller to handle the print
449 | let printController = UIPrintInteractionController.shared
450 | printController.printInfo = printInfo
451 | //Convert the current view to the image that will be printed
452 | printController.printingItem = self.wholePageView.toImage()
453 | //Present the print UI to the user
454 | printController.present(animated: true, completionHandler: nil)
455 | }
456 |
457 | func getSaveStatus() -> Bool {
458 | let policy = IntuneMAMPolicyManager.instance().policy(forAccountId: self.currentUser)
459 | if (nil == policy || (policy?.isSaveToAllowed(for: IntuneMAMSaveLocation.localDrive, withAccountId: self.currentUser))!){
460 | return true
461 | } else {
462 | return false
463 | }
464 | }
465 |
466 | override func viewWillDisappear(_ animated: Bool) {
467 | super.viewWillDisappear(animated)
468 |
469 | //Save the draft message if there is one present
470 | self.saveDraftedMessage()
471 | }
472 |
473 | //Saves the conversation as a text file in the document directory of the application.
474 | func saveConversation(fileName: String, conversation: [(sender: String, message: NSAttributedString)]) {
475 | let newFormat = self.reformatConversation(conversation: conversation)
476 | self.writeFile(fileContent: newFormat, fileName: fileName)
477 | }
478 |
479 | //convert the array of (sender, message) tuples to a single string that represents the entire conversation
480 | func reformatConversation(conversation: [(sender: String, message: NSAttributedString)]) -> String {
481 | var newFormat = ""
482 | for element in conversation {
483 | newFormat.append(element.sender)
484 | newFormat.append(": ")
485 | newFormat.append((element.message).string)
486 | newFormat.append("\n")
487 | }
488 | return newFormat
489 | }
490 |
491 | //write the conversation text to the file in the document directory
492 | func writeFile(fileContent: String, fileName: String) {
493 | do {
494 | let url: URL = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
495 | try fileContent.write(to: url.appendingPathComponent(fileName).appendingPathExtension("txt"), atomically: true, encoding: .utf8)
496 | }catch let error {
497 | print("Error saving file: " + error.localizedDescription)
498 | }
499 | }
500 | }
501 |
502 | extension UIView {
503 | // converts a UIView to an image
504 | // used in the print() function for ChatPage
505 | func toImage() -> UIImage {
506 | // code logic modified from answer by Jody Heavener @ https://stackoverflow.com/questions/32403634/airprint-contents-of-a-uiview
507 | UIGraphicsBeginImageContextWithOptions(bounds.size, false, UIScreen.main.scale)
508 |
509 | drawHierarchy(in: self.bounds, afterScreenUpdates: true)
510 |
511 | let image = UIGraphicsGetImageFromCurrentImageContext()
512 | UIGraphicsEndImageContext()
513 | return image!
514 | }
515 | }
516 |
--------------------------------------------------------------------------------
/chatr/ChatTableViewCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) Microsoft Corporation. All rights reserved.
3 | //
4 |
5 | import UIKit
6 |
7 | class ChatTableViewCell: UITableViewCell {
8 |
9 | @IBOutlet weak var messageView: UITextView!
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/chatr/EnrollmentDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) Microsoft Corporation. All rights reserved.
3 | //
4 |
5 | import IntuneMAMSwift
6 |
7 | /*
8 | This enrollment delegate class can be initialized and set as the enrollment delegate of the IntuneMAMEnrollmentManager
9 | Doing this will trigger the enrollmentRequestWithStatus method whenever an enrollment is attempted.
10 | It can also be used to trigger unenrollRequestWithStatus whenever unenrollment is attempted.
11 | This allows for the app to check if an enrollment was successful
12 |
13 | NOTE: A number of other methods are avaliable in the IntuneMAMEnrollmentDelegate. See documentation or header file for more info.
14 | */
15 | class EnrollmentDelegateClass: NSObject, IntuneMAMEnrollmentDelegate {
16 |
17 | var presentingViewController: UIViewController?
18 |
19 | override init() {
20 | super.init()
21 | self.presentingViewController = nil
22 | }
23 |
24 | /*
25 | To be able to change the view, the class should be initialzed with the curent view controller. Then this view controller can move to the desired view based on the enrollment success
26 |
27 | @param viewController - the view controller this class should use when triggered
28 | */
29 | init(viewController : UIViewController){
30 | super.init()
31 | self.presentingViewController = viewController
32 | }
33 |
34 | /*
35 | This is a method of the delegate that is triggered when an instance of this class is set as the delegate of the IntuneMAMEnrollmentManager and an enrollment is attempted.
36 | The status parameter is a member of the IntuneMAMEnrollmentStatus class. This object can be used to check for the status of an attempted enrollment
37 | If successful, logic for enrollment is initiated
38 | */
39 | func enrollmentRequest(with status: IntuneMAMEnrollmentStatus) {
40 | if status.didSucceed{
41 | //If enrollment was successful, change from the current view (which should have been initialized with the class) to the desired page on the app (in this case ChatPage)
42 | print("Login successful")
43 |
44 | let storyboard = UIStoryboard.init(name: "Main", bundle: nil)
45 | let chatPage = storyboard.instantiateViewController(withIdentifier: "ChatPage")
46 | if nil != self.presentingViewController {
47 | self.presentingViewController!.present(chatPage, animated: false, completion: nil)
48 | } else {
49 | print("Warning: EnrollmentDelegate initialized without a view controller before attempting enrollment.")
50 | UIUtils.getCurrentViewController().present(chatPage, animated: false, completion: nil)
51 | }
52 |
53 | } else if IntuneMAMEnrollmentStatusCode.loginCanceled != status.statusCode {
54 | //In the case of a failure, log failure error status and code
55 | print("Enrollment result for identity \(status.identity) with status code \(status.statusCode)")
56 | print("Debug message: \(String(describing: status.errorString))")
57 |
58 | //Present the user with an alert asking them to sign in again.
59 | let alert = UIAlertController(title: "Error Authenticating", message: "There was an error while logging you into your account. Please check your log in credentials and try again.", preferredStyle: .alert)
60 | let closeAlert = UIAlertAction.init(title: "OK", style: .default, handler: nil)
61 | alert.addAction(closeAlert)
62 | if nil != self.presentingViewController {
63 | self.presentingViewController!.present(alert, animated: true, completion: nil)
64 | } else {
65 | print("Warning: EnrollmentDelegate initialized without a view controller before attempting enrollment.")
66 | UIUtils.getCurrentViewController().present(alert, animated: true, completion: nil)
67 | }
68 | }
69 | }
70 |
71 | /*
72 | This is a method of the delegate that is triggered when an instance of this class is set as the delegate of the IntuneMAMEnrollmentManager and an unenrollment is attempted.
73 | The status parameter is a member of the IntuneMAMEnrollmentStatus class. This object can be used to check for the status of an attempted unenrollment.
74 | Logic for logout/token clearing is initiated here.
75 | */
76 | func unenrollRequest(with status: IntuneMAMEnrollmentStatus) {
77 | //Go back to login page from current view controller
78 | let presentingViewController = UIUtils.getCurrentViewController()
79 | let storyboard = UIStoryboard.init(name: "Main", bundle: nil)
80 | let loginPage = storyboard.instantiateViewController(withIdentifier: "LoginPage")
81 |
82 | presentingViewController.present(loginPage, animated: true, completion: nil)
83 |
84 | if status.didSucceed != true {
85 | //In the case unenrollment failed, log error
86 | print("Unenrollment result for identity \(status.identity) with status code \(status.statusCode)")
87 | print("Debug message: \(String(describing: status.errorString))")
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/chatr/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 | APPL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleURLTypes
20 |
21 |
22 | CFBundleTypeRole
23 | Viewer
24 | CFBundleURLIconFile
25 | logo
26 | CFBundleURLName
27 | chatr
28 | CFBundleURLSchemes
29 |
30 | chatr
31 | chatr-intunemam
32 | msauth.com.microsoft.intunemam
33 |
34 |
35 |
36 | CFBundleVersion
37 | 1
38 | LSApplicationQueriesSchemes
39 |
40 | mvisionmobile
41 | microsoft-edge-http
42 | microsoft-edge-http-intunemam
43 | scmx
44 | lookoutwork-ase
45 | microsoft-edge-https
46 | microsoft-edge-https-intunemam
47 | lacoonsecurity
48 | zips
49 | skycure
50 | smsec
51 | smart-ns
52 | betteractiveshield
53 | companyportal
54 | ms-outlook
55 | ms-outlook-intunemam
56 | wandera
57 | https-intunemam
58 | http-intunemam
59 |
60 | LSRequiresIPhoneOS
61 |
62 | UILaunchStoryboardName
63 | LaunchScreen
64 | UIMainStoryboardFile
65 | Main
66 | UIRequiredDeviceCapabilities
67 |
68 | armv7
69 |
70 | UISupportedInterfaceOrientations
71 |
72 | UIInterfaceOrientationPortrait
73 |
74 | UISupportedInterfaceOrientations~ipad
75 |
76 | UIInterfaceOrientationPortrait
77 | UIInterfaceOrientationPortraitUpsideDown
78 | UIInterfaceOrientationLandscapeLeft
79 | UIInterfaceOrientationLandscapeRight
80 |
81 | UIUserInterfaceStyle
82 | Light
83 |
84 |
85 |
--------------------------------------------------------------------------------
/chatr/KeychainManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) Microsoft Corporation. All rights reserved.
3 | //
4 |
5 | import Foundation
6 |
7 | /*
8 | Class with methods used to manage adding, updating, and removing user data to the keychain.
9 | The keychain is used in this app to securely store user messages and drafted messages.
10 | The data in the keychain persists across app instances unless deleted from the keychain.
11 | */
12 | @objc public class KeychainManager : NSObject{
13 |
14 | //Keys used in keychain to distinguish draft messages from sent messages
15 | static let draftMessageKey: String = "draftMessage"
16 | static let sentMessageKey: String = "sentMessages"
17 |
18 | /*
19 | This function updates a user's current item in the keychain with new data
20 | @param user: the String representing the upn of the user to update the keychain for
21 | @param data: the data to be stored in the keychain, can be of any type
22 | @param key: a string representing the key for the data
23 | */
24 | private class func updateItem(forUser: String, data: Any, key:String){
25 | //Convert given data to archived data that can be stored in the keychain
26 | var archivedData:Data
27 | do {
28 | archivedData = try NSKeyedArchiver.archivedData(withRootObject: data, requiringSecureCoding:true)
29 | }
30 | catch {
31 | print("Unexpected error when updating data in the keychain: Failed to create archived data.")
32 | return
33 | }
34 |
35 | //First define a query that will locate the user's keychain item
36 | let query = [kSecClass: kSecClassGenericPassword,
37 | kSecAttrAccount: forUser,
38 | kSecAttrService: key,
39 | ] as CFDictionary
40 |
41 | //Next define the attributes to be updated (the archived data)
42 | let attributes = [kSecValueData: archivedData] as CFDictionary
43 |
44 | //Update the item in the keychain
45 | let status = SecItemUpdate(query, attributes)
46 | if status != errSecSuccess {
47 | //In the case of an unexpected error, log the error information
48 | print("Unexpected error when updating data in the keychain.")
49 | print("Error code: \(Int(status))")
50 | }
51 | }
52 |
53 | /*
54 | This function is used to retrieve the current sent messages in the keychain for a given user
55 | If any are found, the function returns an array of the sent messages.
56 | If none are found, or there is an error in searching for the item, the function returns nil.
57 | @param forUser: the String representing the upn of the user to search the keychain for
58 | */
59 | @objc public class func getSentMessages(forUser: String) -> [String]?{
60 | //A query specific to the user is defined
61 | //Within the query, kSecReturnData is set to true so that the search will return the message data
62 | let query = [kSecClass: kSecClassGenericPassword,
63 | kSecAttrAccount: forUser,
64 | kSecAttrService: self.sentMessageKey,
65 | kSecReturnData: true,
66 | kSecReturnAttributes: true,
67 | ] as CFDictionary
68 |
69 | var item: CFTypeRef?
70 | //Call the search to find a matching item in the keychain
71 | let status = SecItemCopyMatching(query, &item)
72 | //If no item is found, return nil
73 | guard status != errSecItemNotFound else { return nil }
74 |
75 | if status == errSecSuccess {
76 | //If an item is successfully returned, retrieve the data from the item
77 | guard let queryReturn = item as? [String : Any]
78 | else {
79 | //If the data is not in the appropriate format, log this and return nil
80 | print("Unexpected data returned from keychain")
81 | return nil
82 | }
83 | //Convert the data back to an array and return it
84 | let encodedData = queryReturn[kSecValueData as String] as? Data
85 | do {
86 | let messageArray = try NSKeyedUnarchiver.unarchivedObject(ofClass: NSArray.self, from: encodedData!) as? [String]
87 | return messageArray
88 | }
89 | catch {
90 | print("Unexpected error when searching for data in the keychain: Failed to unarchive sent messages.")
91 | return nil
92 | }
93 | } else {
94 | //Some other unexpected error occurred
95 | print("Unexpected error when searching for data in the keychain.")
96 | print("Error code: \(Int(status))")
97 | return nil
98 | }
99 | }
100 |
101 | /*
102 | This function is used to retrieve the current drafted message in the keychain for a given user
103 | If one is found, it is returned as an String.
104 | If no drafted message is found, or there is an error in searching for the item, the function returns nil.
105 | @param forUser: the String representing the upn of the user to search the keychain for
106 | */
107 | @objc public class func getDraftedMessage(forUser: String) -> String?{
108 | //A query specific to the user is defined
109 | //Within the query, kSecReturnData is set to true so that the search will return the message data
110 | let query = [kSecClass: kSecClassGenericPassword,
111 | kSecAttrAccount: forUser,
112 | kSecAttrService: self.draftMessageKey,
113 | kSecReturnData: true,
114 | kSecReturnAttributes: true,
115 | ] as CFDictionary
116 |
117 | var item: CFTypeRef?
118 | //Call the search to find a matching item in the keychain
119 | let status = SecItemCopyMatching(query, &item)
120 | //If no item is found, return nil
121 | guard status != errSecItemNotFound else { return nil }
122 |
123 | if status == errSecSuccess {
124 | //If an item is successfully returned, retrieve the data from the item
125 | guard let queryReturn = item as? [String : Any]
126 | else {
127 | //If the data is not in the appropriate format, log this and return nil
128 | print("Unexpected data returned from keychain")
129 | return nil
130 | }
131 | //Convert the message back to a string and return it
132 | let encodedData = queryReturn[kSecValueData as String] as? Data
133 | do {
134 | let draftMessage = try NSKeyedUnarchiver.unarchivedObject(ofClass: NSString.self, from: encodedData!) as String?
135 | return draftMessage
136 | }
137 | catch {
138 | print("Unexpected error when searching for data in the keychain: Failed to unarchive draft message.")
139 | return nil
140 | }
141 | } else {
142 | //Some other unexpected error occurred
143 | print("Unexpected error when searching for data in the keychain.")
144 | print("Error code: \(Int(status))")
145 | return nil
146 | }
147 | }
148 |
149 | /*
150 | This function is used to store a sent message in the keychain. It first searches to see if any sent messages are present for the given user already.
151 | If the user already has an item in the keychain for sent messages, update this item. Otherwise, create a new keychain item.
152 | @param forUser: the String representing the upn of the user to add a sent message for
153 | @param message: the String containing the sent message to be added to the keychain
154 | */
155 | @objc public class func storeSentMessage(sentMessage newMessage:String, forUser:String){
156 | if var currentMessages : [String] = KeychainManager.getSentMessages(forUser: forUser) {
157 | //When dealing with sent messages, add the new message to the existing messages if any are present
158 | currentMessages.append(newMessage)
159 | KeychainManager.updateItem(forUser: forUser, data: currentMessages, key: self.sentMessageKey)
160 |
161 | } else {
162 | //Otherwise define a query to add an item for the user containing the sent message within an array (so that future messages can be added)
163 | let messageArray = [newMessage]
164 | var messageData:Data
165 | do {
166 | messageData = try NSKeyedArchiver.archivedData(withRootObject: messageArray, requiringSecureCoding:true)
167 | }
168 | catch {
169 | print("Unexpected error when adding data to the keychain: Failed to archive sent messages.")
170 | return
171 | }
172 |
173 | let query = [kSecClass: kSecClassGenericPassword,
174 | kSecAttrAccount: forUser,
175 | kSecAttrService: self.sentMessageKey,
176 | kSecValueData: messageData,
177 | ] as CFDictionary
178 |
179 | //Add the new item to the query
180 | let status = SecItemAdd(query, nil)
181 | if status != errSecSuccess {
182 | //Some other unexpected error occured
183 | print("Error occurred when adding data to the keychain.")
184 | print("Error code: \(Int(status))")
185 | }
186 | }
187 | }
188 |
189 | /*
190 | This function is used to store a draft message in the keychain. It first searches to see if any draft messages are present for the given user already.
191 | If the user already has an draft message in the keychain, replace this draft message with the new one. Otherwise, create a new keychain item.
192 | @param forUser: the String representing the upn of the user to add a draft message for
193 | @param message: the String containing the draft message to be added to the keychain
194 | */
195 | @objc public class func storeDraftMessage(draftMessage newMessage:String, forUser:String){
196 | if nil != KeychainManager.getDraftedMessage(forUser: forUser) {
197 | //When dealing with draft messages, do not add the new draft message to the existing one, but store only the newest value as only one draft message is stored at a time
198 | KeychainManager.updateItem(forUser: forUser, data: newMessage, key:self.draftMessageKey)
199 |
200 | } else {
201 | //Otherwise define a query to add an item for the user containing the draft message
202 | var messageData:Data
203 |
204 | do {
205 | messageData = try NSKeyedArchiver.archivedData(withRootObject: newMessage, requiringSecureCoding:true)
206 | }
207 | catch {
208 | print("Unexpected error when adding data to the keychain: Failed to create archive draft message.")
209 | return
210 | }
211 |
212 | let query = [kSecClass: kSecClassGenericPassword,
213 | kSecAttrAccount: forUser,
214 | kSecAttrService: self.draftMessageKey,
215 | kSecValueData: messageData,
216 | ] as CFDictionary
217 |
218 | //Add the new item to the query
219 | let status = SecItemAdd(query, nil)
220 | if status != errSecSuccess {
221 | //Some other unexpected error occured
222 | print("Error occurred when adding data to the keychain.")
223 | print("Error code: \(Int(status))")
224 | }
225 | }
226 | }
227 |
228 | /*
229 | Function used to delete the sent messages data for a given user from the keychain.
230 | Returns true if the deletion was successful, and false if the deletion failed.
231 | @param forUser: the String representing the upn of the user to remove keychain items for
232 | */
233 | @objc public class func deleteSentMessages(forUser:String) -> Bool{
234 |
235 | //Define a query that will locate the sent messages item in the keychain
236 | let query = [kSecClass: kSecClassGenericPassword,
237 | kSecAttrAccount: forUser,
238 | kSecAttrService: self.sentMessageKey,
239 | ] as CFDictionary
240 |
241 | //Delete the item the query finds
242 | let status = SecItemDelete(query)
243 | //Even if the item was not found, this is considered successful as the item may never have been there
244 | if status != errSecSuccess && status != errSecItemNotFound {
245 | print("Error occurred when removing sent messages from the keychain.")
246 | print("Error code: \(Int(status))")
247 | return false
248 | }
249 | return true
250 | }
251 |
252 | /*
253 | Function used to delete the draft message data for a given user from the keychain.
254 | Returns true if the deletion was successful, and false if the deletion failed.
255 | @param forUser: the String representing the upn of the user to remove keychain items for
256 | */
257 | @objc public class func deleteDraftMessage(forUser:String) -> Bool{
258 |
259 | //Define a query that will locate the draft message item in the keychain
260 | let query = [kSecClass: kSecClassGenericPassword,
261 | kSecAttrAccount: forUser,
262 | kSecAttrService: self.draftMessageKey,
263 | ] as CFDictionary
264 |
265 | //Delete the item the query finds
266 | let status = SecItemDelete(query)
267 | //Even if the item was not found, this is considered successful as the item may never have been there
268 | if status != errSecSuccess && status != errSecItemNotFound {
269 | print("Error occurred when removing the draft message from the keychain.")
270 | print("Error code: \(Int(status))")
271 | return false
272 | }
273 | return true
274 | }
275 | }
276 |
--------------------------------------------------------------------------------
/chatr/LoginPage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) Microsoft Corporation. All rights reserved.
3 | //
4 |
5 | import IntuneMAMSwift
6 |
7 | class LoginPage: UIViewController {
8 | /*
9 | Button action triggered when user presses the log in button
10 |
11 | Sends the user through the Intune Authentication flow for logging in.
12 | Triggers the "homePage" segue if login is successful; raises an alert if there is an error.
13 | */
14 | @IBAction func logInBtn(_ sender: Any) {
15 | //Set the delegate of the IntuneMAMEnrollmentManager as an instance of the EnrollmentDelegateClass to check the status of attempted enrollments. Also initialize this class with the current view controller
16 | //This is done on launch, but here it is done again to give the delegate the current view controller
17 | let enrollmentDelegate = EnrollmentDelegateClass.init(viewController: self)
18 | IntuneMAMEnrollmentManager.instance().delegate = enrollmentDelegate
19 |
20 | //Login the user through the Intune sign in flow. EnrollmentDelegateClass will handle the outcome of this.
21 | IntuneMAMEnrollmentManager.instance().loginAndEnrollAccount(nil)
22 | }
23 | }
24 |
25 |
--------------------------------------------------------------------------------
/chatr/PolicyDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) Microsoft Corporation. All rights reserved.
3 | //
4 |
5 | import IntuneMAMSwift
6 |
7 | /*
8 | This policy delegate class can be initialized and set as the delegate of the IntuneMAMPolicyManager
9 | (This is done in the AppDelegate.swift file at app initialization)
10 | Doing this will trigger the wipeDataForAccount and restartApplication methods whenever the Intune SDK needs to do either of these things
11 |
12 | NOTE: A number of other methods are avaliable in the IntuneMAMPolicyDelegate. See documentation or header file for more info.
13 | Methods like identitySwitchRequired and addIdentity are generally used in multi-user apps, unlike this single user app.
14 | See IntuneMAMPolicyDelegate documentation or header file for more information
15 | */
16 |
17 | class PolicyDelegateClass: NSObject, IntuneMAMPolicyDelegate {
18 |
19 | /*
20 | wipeDataForAccount is called by the Intune SDK when the app needs to wipe all the data for a specified user
21 | With chatr, the only user data stored are the sent chat messages and drafted chat messages.
22 | If this is wiped successfully, return true, otherwise return false
23 |
24 | @param upn is the upn of the user whoes data is to be wiped (for example "user@example.com")
25 | */
26 | func wipeData(forAccountId: String) -> Bool {
27 | //variable to track if the data wipe was successful
28 | var wipeSuccess = true
29 |
30 | //remove all files in each directory
31 | let fileManager: FileManager = FileManager.default
32 | let paths: [String] = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
33 | for fileDirectory in paths {
34 | do {
35 | let fileArray = try fileManager.contentsOfDirectory(atPath: fileDirectory)
36 | for fileName in fileArray {
37 | let fileDirectoryURL = URL(fileURLWithPath: fileDirectory)
38 | let fullPathURL = fileDirectoryURL.appendingPathComponent(fileName)
39 | try fileManager.removeItem(atPath: fullPathURL.path)
40 | }
41 | } catch {
42 | print("Could not successfully remove files from directory. Error: \(error)")
43 | wipeSuccess = false
44 | }
45 | }
46 |
47 | //Use the deleteSentMessagesForUser and deleteSentMessagesForUser functions in the KeychainManager class to look into the keychain to wipe any message data stored for a given upn
48 | if !(KeychainManager.deleteDraftMessage(forUser: forAccountId) && KeychainManager.deleteSentMessages(forUser: forAccountId)){
49 | //If either function call returns false, this indicates the app failed clear the user's messages from the keychain
50 | print("Data wipe from keychain failed")
51 | wipeSuccess = false
52 | }
53 |
54 | return wipeSuccess
55 | }
56 |
57 | /*
58 | In the case that the app needs to perform tasks like save user data before the Intune SDK restarts the app, those tasks can be done here
59 | With Chatr, drafted messages need to be saved if a restart is forced.
60 |
61 | If the app will handle restarting on its own, return true.
62 | If the app wants the Intune SDK to handle the restart, @return false.
63 | See IntuneMAMPolicyDelegate documentation or header file for more information
64 | */
65 | func restartApplication() -> Bool {
66 | //If the current view is the chat page and there is a message currently being drafted, then save it to the keychain to repopulate it after the restart.
67 | let currentViewController = UIUtils.getCurrentViewController()
68 | if currentViewController is ChatPage{
69 | //Call saveDraftedMessage on the ChatPage to save the drafted message to the keychain if there is one present.
70 | let page = currentViewController as! ChatPage
71 | page.saveDraftedMessage()
72 | }
73 | return false
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/chatr/SettingsPage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) Microsoft Corporation. All rights reserved.
3 | //
4 |
5 | import IntuneMAMSwift
6 |
7 | class SettingsPage: UIViewController {
8 | @IBOutlet weak var displayConsoleButton: UIButton!
9 | @IBOutlet weak var backButton: UIButton!
10 |
11 | override func viewDidLoad() {
12 | self.backButton.setImage(#imageLiteral(resourceName: "backarrow.png"), for: UIControl.State.normal)
13 | self.backButton.imageView!.contentMode = .scaleAspectFit
14 | self.backButton.addTarget(self, action: #selector (self.dismiss), for: .touchUpInside)
15 | }
16 |
17 | //Listener for diagnostic console button
18 | @IBAction func displayConsoleTapped(_ sender: Any) {
19 | //when button is tapped, display console
20 | IntuneMAMDiagnosticConsole.display()
21 | }
22 |
23 | //Button action triggered when back button is pressed on the settings page
24 | //Dismisses the view.
25 | @IBAction func dismiss(_ sender: UIButton) {
26 | self.dismiss(animated: true, completion: nil)
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/chatr/SideBarTableCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) Microsoft Corporation. All rights reserved.
3 | //
4 |
5 | import UIKit
6 |
7 | class SideBarTableCell: UITableViewCell {
8 |
9 | @IBOutlet weak var cellLbl: UILabel!
10 | @IBOutlet weak var cellImg: UIImageView!
11 | }
12 |
--------------------------------------------------------------------------------
/chatr/UIUtils.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) Microsoft Corporation. All rights reserved.
3 | //
4 |
5 | import UIKit
6 |
7 | class UIUtils{
8 |
9 | class func getCurrentViewController() -> UIViewController{
10 | var topController = UIApplication.shared.keyWindow?.rootViewController
11 | if (nil != topController) {
12 | var presentedViewController = topController!.presentedViewController
13 | //Loop until there are no more view controllers to go to
14 | while (nil != presentedViewController){
15 | topController = presentedViewController
16 | presentedViewController = topController!.presentedViewController
17 | }
18 | }
19 | //Return the final view controller
20 | return topController!
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/chatr/chatr.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | keychain-access-groups
6 |
7 | $(AppIdentifierPrefix)Intune.chatr
8 | $(AppIdentifierPrefix)com.microsoft.intune.mam
9 | $(AppIdentifierPrefix)com.microsoft.adalcache
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/logo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/microsoftconnect/Chatr-Sample-Intune-iOS-App/01c0eac8c72e9afc8f4097754a1fce799b0dbd03/logo.jpg
--------------------------------------------------------------------------------