-
Notifications
You must be signed in to change notification settings - Fork 0
/
MessagesViewController.swift
362 lines (313 loc) · 15.6 KB
/
MessagesViewController.swift
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
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
236
237
238
239
240
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
268
269
270
271
272
273
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
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
//
// MessagesViewController.swift
// imUrlData MessagesExtension
//
// Created by Andrew Dent on 12/1/19.
// Copyright © 2019 Touchgram Pty Ltd. All rights reserved.
//
import UIKit
import os
import Messages
import AVFoundation
class MessagesViewController: MSMessagesAppViewController, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
@IBOutlet fileprivate weak var useCameraBtn: UIButton!
@IBOutlet fileprivate weak var pickFromRollBtn: UIButton!
@IBOutlet fileprivate weak var sendBtn: UIButton!
@IBOutlet fileprivate weak var sendAtchBtn: UIButton!
@IBOutlet fileprivate weak var imageView: UIImageView!
@IBOutlet weak var tintView: UIView!
@IBOutlet weak var buttonStack: UIStackView!
@IBOutlet weak var imageDetails: UILabel!
var imagePickerController = UIImagePickerController()
var currentPicker = UIImagePickerController.SourceType.photoLibrary
var capturedImages = [UIImage]()
var attachmentPath : URL? = nil
var amShowingReceivedPhoto = false // flag so when we show full window know if should put up picker
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
imagePickerController.modalPresentationStyle = .currentContext
imagePickerController.delegate = self
}
func showReceivedImage(_ img:UIImage) {
amShowingReceivedPhoto = true
imageDetails.isHidden = false
imageDetails.text = img.debugDescription
tintView.isHidden = true
buttonStack.isHidden = true
imageView.isHidden = false
imageView.image = img
sendBtn.isEnabled = false
sendAtchBtn.isEnabled = false
pickFromRollBtn.isEnabled = false
}
func printDetails(_ msg:MSMessage, from:String) {
if msg.layout == nil {
print("\(from) \(msg.debugDescription)\n has no layout")
}
else {
if let lay = msg.layout as? MSMessageTemplateLayout {
if let im = lay.image {
showReceivedImage(im)
print("\(from) \(msg.debugDescription)\n with image in layout\n \(im.debugDescription) ")
} else {
print("\(from) \(msg.debugDescription)\n with layout\n \(lay.debugDescription) BUT NO IMAGE")
}
} else {
print("\(from) \(msg.debugDescription)\n has a layout but cannot cast to MSMessageTemplateLayout")
}
}
}
// MARK: - Conversation Handling
override func willBecomeActive(with conversation: MSConversation) {
// Called when the extension is about to move from the inactive to active state.
// This will happen when the extension is about to present UI.
// NOTE that means you may have launched the extension to compose a new message OR selected previous, which also hits didSelect
// Use this method to configure the extension and restore previously stored state.
}
override func didBecomeActive(with conversation: MSConversation) {
guard let sel = conversation.selectedMessage else {
os_log("didBecomeActive with no selectedMessage in conversation")
return
}
printDetails(sel, from:"didBecomeActive")
// nothing to process as image is in the message layout
}
override func didResignActive(with conversation: MSConversation) {
// Called when the extension is about to move from the active to inactive state.
// This will happen when the user dissmises the extension, changes to a different
// conversation or quits Messages.
// Use this method to release shared resources, save user data, invalidate timers,
// and store enough state information to restore your extension to its current state
// in case it is terminated later.
}
override func didSelect(_ message: MSMessage, conversation: MSConversation) {
os_log("didSelect")
printDetails(message, from:"didSelect")
super.didSelect(message, conversation: conversation)
// nothing to process as image is in the message layout
}
override func didReceive(_ message: MSMessage, conversation: MSConversation) {
// Called when a message arrives that was generated by another instance of this
// extension on a remote device. ONLY if the message arrives whilst
// this extension is active (ie: composing a new message with it)
// nothing to process as image is in the message layout
guard let sel = conversation.selectedMessage else {
os_log("didReceive with no selectedMessage in conversation")
return
}
printDetails(sel, from:"didReceive")
}
override func didStartSending(_ message: MSMessage, conversation: MSConversation) {
// Called when the user taps the send button.
}
override func didCancelSending(_ message: MSMessage, conversation: MSConversation) {
// Called when the user deletes the message without sending it.
// Use this to clean up state related to the deleted message.
}
override func willTransition(to presentationStyle: MSMessagesAppPresentationStyle) {
// Called before the extension transitions to a new presentation style.
// Use this method to prepare for the change in presentation style.
}
override func didTransition(to presentationStyle: MSMessagesAppPresentationStyle) {
// Called after the extension transitions to a new presentation style.
// Use this method to finalize any behaviors associated with the change in presentation style.
if presentationStyle == .expanded && !amShowingReceivedPhoto {
// show the roll if got here by tapping button or just resizing view
showImagePicker(sourceType: currentPicker)
}
}
// MARK: - Comms
func send() {
guard let conversation = activeConversation else { fatalError("Expected a conversation") }
let layout = MSMessageTemplateLayout()
layout.caption = "Your photo from imPhoto sample app"
let session = conversation.selectedMessage?.session
let message = MSMessage(session: session ?? MSSession())
// capturedImages is empty now thanks to end of finishAndUpdate, so use the one from the image
layout.image = imageView!.image! // NOTE in this simple example may find this delivers a flipped image
message.layout = layout // WARNING do this assignment AFTER the image assigned, it's not referential, structures are copied!
conversation.insert(message) { (error) in
if let error = error {
os_log("Error with MSConversation.insert(message)")
print(error)
}
}
dismiss()
}
func sendAttachment() {
guard let conversation = activeConversation else { fatalError("Expected a conversation") }
guard
let imageData = imageView?.image?.jpegData(compressionQuality: 0.8),
let docUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
else {
dismiss()
return
}
// WARNING this is not great practice - not robust if muliple messages sent without completing upload
attachmentPath = URL(fileURLWithPath: "imPhoto.jpg", relativeTo: docUrl)
if (try? imageData.write(to: attachmentPath!)) != nil {
conversation.insertAttachment(attachmentPath!, withAlternateFilename: "imPhoto.jpg") { (error) in
if let error = error {
os_log("Error with insertAttachment(message)")
print(error)
}
}
}
dismiss()
}
func sendableDisplay(enabled:Bool) {
imageView?.isHidden = !enabled
sendBtn?.isEnabled = enabled
sendAtchBtn?.isEnabled = enabled
}
fileprivate func finishAndUpdate() {
dismiss(animated: true, completion: { [weak self] in
guard let `self` = self else {
return
}
if `self`.capturedImages.count > 0 {
`self`.sendableDisplay(enabled: true)
if `self`.capturedImages.count == 1 {
// Camera took a single picture.
`self`.imageView?.image = `self`.capturedImages[0]
} else {
// Camera took multiple pictures; use the list of images for animation.
`self`.imageView?.animationImages = `self`.capturedImages
`self`.imageView?.animationDuration = 5 // Show each captured photo for 5 seconds.
`self`.imageView?.animationRepeatCount = 0 // Animate forever (show all photos).
`self`.imageView?.startAnimating()
}
// To be ready to start again, clear the captured images array.
`self`.capturedImages.removeAll()
}
})
}
/// Copied from Apple's PhotoPicker sample
/// APLViewController.swift
fileprivate func showImagePicker(sourceType: UIImagePickerController.SourceType) {
// If the image contains multiple frames, stop animating.
if (imageView?.isAnimating)! {
imageView?.stopAnimating()
}
if capturedImages.count > 0 {
capturedImages.removeAll()
}
imagePickerController.sourceType = sourceType
imagePickerController.modalPresentationStyle =
(sourceType == UIImagePickerController.SourceType.camera) ?
UIModalPresentationStyle.fullScreen : UIModalPresentationStyle.popover
let presentationController = imagePickerController.popoverPresentationController
/* unlike the PhotoPicker sample, we don't have a UIBarButtonItem
presentationController?.barButtonItem = button // Display popover from the UIBarButtonItem as an anchor.
*/
presentationController?.sourceView = self.view
let b = self.view.bounds
presentationController?.sourceRect = b.insetBy(dx: 8.0, dy: 20.0)
presentationController?.permittedArrowDirections = UIPopoverArrowDirection.any
/*
if sourceType == UIImagePickerControllerSourceType.camera {
// The user wants to use the camera interface. Set up our custom overlay view for the camera.
imagePickerController.showsCameraControls = false
// Apply our overlay view containing the toolar to take pictures in various ways.
overlayView?.frame = (imagePickerController.cameraOverlayView?.frame)!
imagePickerController.cameraOverlayView = overlayView
}*/
present(imagePickerController, animated: true, completion: {
// Done presenting.
})
}
func showCurrentPicker() {
if presentationStyle == .compact {
amShowingReceivedPhoto = false // in case still resident, reset the flag
requestPresentationStyle(.expanded) // see didTransition(to:)
} else {
showImagePicker(sourceType: currentPicker)
}
}
// MARK: - Buttons
@IBAction func onUseCamera(_ sender: Any) {
currentPicker = UIImagePickerController.SourceType.camera
let authStatus = AVCaptureDevice.authorizationStatus(for: AVMediaType.video)
if authStatus == AVAuthorizationStatus.denied {
// Denied access to camera, alert the user.
// The user has previously denied access. Remind the user that we need camera access to be useful.
let alert = UIAlertController(title: "Unable to access the Camera",
message: "To enable access, go to Settings > Privacy > Camera and turn on Camera access for this app.",
preferredStyle: UIAlertController.Style.alert)
let okAction = UIAlertAction(title: "OK", style: .cancel, handler: nil)
alert.addAction(okAction)
let settingsAction = UIAlertAction(title: "Settings", style: .default, handler: { _ in
// Take the user to Settings app to possibly change permission.
guard let settingsUrl = URL(string: UIApplication.openSettingsURLString) else { return }
/* normal advice for extensions is
self.extensionContext?.open(settingsUrl, completionHandler: { (success) in ...
extensionContext.open does NOT WORK FROM INSIDE iMessage.
however we can go back up the responder chain to get the iMessage parent app
*/
let openSel = #selector(UIApplication.open(_:options:completionHandler:))
var responder = self as UIResponder?
while (responder != nil){
if responder?.responds(to: openSel ) == true{
// cannot package up arguments, so assume is a UIApplication and cast
(responder as? UIApplication)?.open(settingsUrl, completionHandler:{(success) in
if success {
os_log("Successfully opened settings")
} else {
os_log("Failed to open Settings")
}
})
return
}
responder = responder!.next
}
})
alert.addAction(settingsAction)
present(alert, animated: true, completion: nil)
}
else if (authStatus == AVAuthorizationStatus.notDetermined) {
// The user has not yet been presented with the option to grant access to the camera hardware.
// Ask for permission.
//
// (Note: you can test for this case by deleting the app on the device, if already installed).
// (Note: we need a usage description in our Info.plist to request access.
//
AVCaptureDevice.requestAccess(for: AVMediaType.video, completionHandler: { (granted) in
if granted {
DispatchQueue.main.async {
self.showCurrentPicker()
}
}
})
} else {
showCurrentPicker()
}
}
@IBAction public func onPickFromRoll(_ sender: UIButton) {
currentPicker = UIImagePickerController.SourceType.photoLibrary
showCurrentPicker()
}
// should only be enabled when there is a backgroundImage set
@IBAction public func onSend(_ sender: UIButton) {
send()
imageView.image = nil
sendableDisplay(enabled: false)
}
// should only be enabled when there is a backgroundImage set
@IBAction public func onSendAttachment(_ sender: UIButton) {
sendAttachment()
imageView.image = nil
sendableDisplay(enabled: false)
}
// MARK: - UIImagePickerControllerDelegate
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
guard let image = info[UIImagePickerController.InfoKey.originalImage] else { return }
capturedImages.append(image as! UIImage)
finishAndUpdate()
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
dismiss(animated: true, completion: {
// Done cancel dismiss of image picker.
})
}
}