App-iOS/ConversationViewController.swift
/* |
Copyright (C) 2018 Apple Inc. All Rights Reserved. |
See LICENSE.txt for this sample’s licensing information |
Abstract: |
Displays a conversation in a table view, with support for sending messages. |
*/ |
import UIKit |
/// This class displays a conversation in table view, along with a UI that lets |
/// the user send a message on an online conversation. It is entirely separated |
/// from the networking stack, allow it to be used (and tested!) without any |
/// networking. The only requirement are: |
/// |
/// * Someone must set the `conversation` property when the conversation |
/// changes. |
/// |
/// * Someone should act as the delegate for the sake of both closing the |
/// conversation and sending messages. |
class ConversationViewController : UITableViewController { |
/// The conversation being displayed. Changes to this will update the UI. |
var conversation: Conversation = .empty { |
didSet { |
self.title = self.conversation.localizedName |
guard self.isViewLoaded else { return } |
// If the update has just added new messages we insert those at the |
// end in order to get a cleaner animation. Otherwise we just |
// reload everything from scratch. |
let newMessageCount = self.conversation.messages.count |
let oldMessageCount = oldValue.messages.count |
if oldMessageCount != 0 && newMessageCount > oldMessageCount { |
let newIndexPaths = (oldMessageCount..<newMessageCount).map { IndexPath(row: $0, section: Sections.messages.rawValue) } |
self.tableView.insertRows(at: newIndexPaths, with: .automatic) |
} else { |
self.tableView.reloadSections(IndexSet([Sections.messages.rawValue]), with: .automatic) |
} |
// IMPORTANT: We have to do this reload after dealing with any |
// insertions in the `.messages` section because the reload can |
// trigger a call to our data source methods and those will return |
// info about the new value not the old value. |
if self.conversation.state != oldValue.state { |
self.tableView.reloadSections([Sections.status.rawValue], with: .automatic) |
} |
} |
} |
typealias Delegate = ConversationViewControllerDelegate |
weak var delegate: Delegate? = nil |
// MARK: - Table View Callbacks |
override func numberOfSections(in tableView: UITableView) -> Int { |
return Sections.status.rawValue + 1 |
} |
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { |
switch Sections(rawValue: section)! { |
case .messages: return max(self.conversation.messages.count, 1) |
case .status: return 1 |
} |
} |
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { |
switch (Sections(rawValue: indexPath.section)!, indexPath.row, self.conversation.state) { |
case (.messages, 0, _) where self.conversation.messages.isEmpty: |
return tableView.dequeueReusableCell(withIdentifier: "none", for: indexPath) |
case (.messages, _, _): |
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! MessageCell |
cell.message = self.conversation.messages[indexPath.row] |
return cell |
case (.status, 0, .online): |
let cell = tableView.dequeueReusableCell(withIdentifier: "compose", for: indexPath) as! ComposeCell |
cell.parent = self |
return cell |
case (.status, 0, .offline(let localizedStatus)): |
let cell = tableView.dequeueReusableCell(withIdentifier: "offline", for: indexPath) |
cell.textLabel!.text = localizedStatus |
return cell |
default: |
fatalError() |
} |
} |
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { |
self.tableView.deselectRow(at: indexPath, animated: true) |
} |
// MARK: - Internals |
private enum Sections : Int { |
case messages |
case status |
} |
/// This is called by `ComposeCell` when there’s a message to send. I could have made |
/// a `ComposeCell.Delegate` to handle this, but the reality is that `ComposeCell` and |
/// this class are super tightly bound already so I don’t feel so bad about having it |
/// call us like this. |
/// |
/// - Parameter message: The message to send. |
func sendMessage(message: String) { |
self.delegate?.send(message: message, conversationViewController: self) |
} |
/// Wired up to the Done button. |
@IBAction private func doneAction(_ sender: Any) { |
self.delegate?.close(conversationViewController: self) |
} |
} |
/// Delegate protocol for `ConversationViewController`. |
protocol ConversationViewControllerDelegate : AnyObject { |
/// Called when the user wants to send a message. The delegate is expected to |
/// send the message _and_ reflect it back in `conversation.messages`. |
/// |
/// - Parameters: |
/// - message: The message to send. |
/// - conversationViewController: The sender. |
func send(message: String, conversationViewController: ConversationViewController) |
/// Called when the user indicates that they want to close the conversation. |
/// |
/// - Parameter conversationViewController: The sender. |
func close(conversationViewController: ConversationViewController) |
} |
/// A cell for the `.messages` section of the table view. This has the smarts |
/// necessary to render a message in some reasonably pleasing way. |
/// |
/// - note: I’d prefer if this were private but that causes a runtime failure.. |
class MessageCell : UITableViewCell { |
var message: Message? = nil { |
didSet { |
guard let message = self.message else { |
self.messageLabel.text = "_N/A_" // not localised because it should never be seen |
return |
} |
let format: String |
switch message.direction { |
case .incoming: format = NSLocalizedString("_<-_%@_", comment: "Format for incoming message in the messages view.") |
case .outgoing: format = NSLocalizedString("_->_%@_", comment: "Format for outgoing message in the messages view.") |
} |
self.messageLabel.text = String(format: format, message.text) |
} |
} |
@IBOutlet private var messageLabel: UILabel! |
} |
/// A cell for the `.status` secton of the conversation table view. This runs a |
/// text field in which the user can enter messages. |
/// |
/// - note: I’d prefer if this were private but that causes a runtime failure.. |
class ComposeCell : UITableViewCell, UITextFieldDelegate { |
/// A reference to the parent view controller. When a message is reading to |
/// go this cell calls that view controller’s `sendMessage(message:)` |
/// method. I could have used a delegate here but deemed that wasn’t |
/// necessary as this class is already tightly couple to the view |
/// controller. |
weak var parent: ConversationViewController? = nil |
func textFieldShouldReturn(_ textField: UITextField) -> Bool { |
if let message = self.messageField.text, !message.isEmpty, let parent = self.parent { |
self.messageField.text = "" |
parent.sendMessage(message: message) |
} |
return false |
} |
@IBOutlet private var messageField: UITextField! |
} |
Copyright © 2018 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2018-05-10