diff --git a/chat-iOS/Views/CreateChatRoom/CreateChatRoomViewBuilder.swift b/chat-iOS/Views/CreateChatRoom/CreateChatRoomViewBuilder.swift new file mode 100644 index 0000000..a275e39 --- /dev/null +++ b/chat-iOS/Views/CreateChatRoom/CreateChatRoomViewBuilder.swift @@ -0,0 +1,20 @@ +// +// CreateChatRoomViewBuilder.swift +// chat-iOS +// +// Created by jun on 2020/07/18. +// + +import UIKit + +struct CreateChatRoomViewBuilder { + static func create() -> UIViewController { + guard let createChatRoomViewController = CreateChatRoomViewController.loadFromStoryboard() as? CreateChatRoomViewController else { + fatalError("fatal: Failed to initialize the CreateChatRoomViewController") + } + let model = CreateChatRoomModel() + let presenter = CreateChatRoomViewPresenter(model: model) + createChatRoomViewController.inject(with: presenter) + return createChatRoomViewController + } +} diff --git a/chat-iOS/Views/CreateChatRoom/CreateChatRoomViewController.swift b/chat-iOS/Views/CreateChatRoom/CreateChatRoomViewController.swift new file mode 100644 index 0000000..4c0ccb6 --- /dev/null +++ b/chat-iOS/Views/CreateChatRoom/CreateChatRoomViewController.swift @@ -0,0 +1,230 @@ +// +// File.swift +// chat-iOS +// +// Created by jun on 2020/07/18. +// + +import UIKit + +final class CreateChatRoomViewController: UIViewController { + private var presenter: CreateChatRoomViewPresenterProtocol! + + @IBOutlet weak var userNameSearchBar: UISearchBar! + @IBOutlet weak var serchUserTableview: UITableView! + @IBOutlet weak var selectedUserCollectionView: UICollectionView! + + @IBOutlet weak var selectedUserCollectionViewBottomsConstraints: NSLayoutConstraint! + + var activityIndicator = UIActivityIndicatorView() + + private let searchedUsersCellID = "SearchUserTableviewCell" + private let selectedUsersCellID = "SelectedUserCollectionViewCell" + + override func viewDidLoad() { + super.viewDidLoad() + + self.setupNavigationItem() + self.setupUserSearchBar() + self.setupSerchUserTableview() + self.setupSelectedUserCollectionView() + self.setupActivityIndicator() + self.setupNotificationCenter() + } + + private func setupNavigationItem() { + self.navigationItem.title = "Choose friends" + let stopItem = UIBarButtonItem(barButtonSystemItem: .stop, target: self, action: #selector(tapStopCreateRoomButton)) + self.navigationItem.leftBarButtonItem = stopItem + let saveItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(tapCreateRoomButton)) + self.navigationItem.rightBarButtonItem = saveItem + } + + private func setupUserSearchBar() { + self.userNameSearchBar.placeholder = "Search by name" + self.userNameSearchBar.delegate = self + } + + private func setupSerchUserTableview() { + self.serchUserTableview.rowHeight = 75 + self.serchUserTableview.delegate = self + self.serchUserTableview.dataSource = self + self.serchUserTableview.tableFooterView = UIView() + } + + private func setupSelectedUserCollectionView() { + self.selectedUserCollectionView.isHidden = true + self.selectedUserCollectionView.collectionViewLayout.invalidateLayout() + self.selectedUserCollectionView.delegate = self + self.selectedUserCollectionView.dataSource = self + } + + private func setupActivityIndicator() { + self.activityIndicator.frame = CGRect(x: 0, y: 0, width: 50, height: 50) + self.activityIndicator.center = self.view.center + self.activityIndicator.hidesWhenStopped = true + self.view.addSubview(self.activityIndicator) + } + + func setupNotificationCenter() { + NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillShowNotification(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillHideNotification(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil) + } + + /// キーボードが登場した時の処理 + @objc func keyboardWillShowNotification(notification: NSNotification) { + guard let userInfo = notification.userInfo else { return } + guard let keyboard = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return } + + self.selectedUserCollectionViewBottomsConstraints.constant = -keyboard.cgRectValue.height + self.view.safeAreaInsets.bottom + UIView.animate(withDuration: 1.0, animations: { self.view.layoutIfNeeded() }) + } + + /// キーボードが隠れた時の処理 + @objc func keyboardWillHideNotification(notification: NSNotification) { + guard notification.userInfo != nil else { return } + + self.selectedUserCollectionViewBottomsConstraints.constant = 0 + UIView.animate(withDuration: 1.0, animations: { self.view.layoutIfNeeded() }) + } + + @objc func tapStopCreateRoomButton() { + self.presenter.didTapStopCreateRoomButton() + } + + @objc func tapCreateRoomButton() { + self.presenter.didTapCreateRoomutton() + } + + func inject(with presenter: CreateChatRoomViewPresenterProtocol) { + self.presenter = presenter + self.presenter.view = self + } +} + +extension CreateChatRoomViewController: CreateChatRoomViewPresenterOutput { + func reloadSerchUserTableview() { + DispatchQueue.main.async { self.serchUserTableview.reloadData() } + } + + func reloadSelectedUserCollectionView() { + DispatchQueue.main.async { self.selectedUserCollectionView.reloadData() } + guard self.selectedUserCollectionView.isHidden else { return } + + self.selectedUserCollectionView.alpha = 0 + self.selectedUserCollectionView.isHidden = false + UIView.animate( withDuration: 0.15, animations: { self.selectedUserCollectionView.alpha = 1 }, completion: nil) + } + + func hiddenSelectedUsersCollectionView() { + UIView.animate( + withDuration: 0.25, + animations: { self.selectedUserCollectionView.alpha = 0 }, + completion: { _ in + self.selectedUserCollectionView.isHidden = true + self.selectedUserCollectionView.alpha = 1 + } + ) + } + + func dismissCreateChatRoomVC() { + DispatchQueue.main.async { self.dismiss(animated: true, completion: nil) } + } + + func clearSearchUserTableView() { + DispatchQueue.main.async { self.serchUserTableview.reloadData() } + } + + func startActivityIndicator() { + DispatchQueue.main.async { self.activityIndicator.startAnimating() } + } + + func stopActivityIndicator() { + DispatchQueue.main.async { self.activityIndicator.stopAnimating() } + } +} + +extension CreateChatRoomViewController: UITableViewDelegate, UITableViewDataSource { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return self.presenter.numberOfSearchedUsers + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = self.serchUserTableview.dequeueReusableCell(withIdentifier: self.searchedUsersCellID, for: indexPath) + as? SearchUserTableviewCell else { return UITableViewCell() } + + let user = presenter.searchedUsers[indexPath.item] + let isSelected = presenter.isSelected(user: user) + cell.configure(with: user, isSelected: isSelected) + + //TODO:Firestoreから取得した後で表示し直すこと + if #available(iOS 13.0, *) { + cell.profileImageView.image = UIImage(systemName: "bolt.circle.fill") + } else { + // Fallback on earlier versions + } + + return cell + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let searchedUsersArray: [User] = self.presenter.searchedUsers + self.serchUserTableview.deselectRow(at: indexPath, animated: true) + self.presenter.didSelectedSerchUserTableview(selectedUser: searchedUsersArray[indexPath.item]) + } +} + +extension CreateChatRoomViewController: UICollectionViewDelegate, UICollectionViewDataSource { + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return self.presenter.numberOfSelectedUsers + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: selectedUsersCellID, for: indexPath) as! SelectedUserCollectionViewCell + + cell.configure(with: self.presenter.selectedUsers[indexPath.item]) + cell.deleteUserButtonAction = { [weak self] in + self?.presenter.didTapSelectedUserCollectionViewCellDeleteUserButton(index: indexPath.item) + } + + //TODO:Firestoreから取得した後で表示し直すこと + if #available(iOS 13.0, *) { + cell.profileImageView.image = UIImage(systemName: "bolt.circle.fill") + } else { + // Fallback on earlier versions + } + + return cell + } + + func numberOfSections(in collectionView: UICollectionView) -> Int { + return 1 + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + return CGSize(width: 85, height: 90) + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { + return UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 10) + } +} + +extension CreateChatRoomViewController: UISearchBarDelegate { + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + guard scrollView == self.serchUserTableview else { return } + guard self.userNameSearchBar.isFirstResponder else { return } + self.userNameSearchBar.resignFirstResponder() + } + + /// 検索ボタンがタップされたときに呼ばれる関数 + /// - Parameter searchBar: サーチバー + func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { + guard let searchBarText = searchBar.text else { return } + guard !searchBarText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return } + guard self.userNameSearchBar.isFirstResponder else { return } + self.userNameSearchBar.resignFirstResponder() + + self.presenter.didSearchBarSearchButtonClicked(searchText: searchBarText) + } +} diff --git a/chat-iOS/Views/CreateChatRoom/CreateChatRoomViewModel.swift b/chat-iOS/Views/CreateChatRoom/CreateChatRoomViewModel.swift new file mode 100644 index 0000000..26d210d --- /dev/null +++ b/chat-iOS/Views/CreateChatRoom/CreateChatRoomViewModel.swift @@ -0,0 +1,129 @@ +// +// CreateChatRoomViewModel.swift +// chat-iOS +// +// Created by jun on 2020/07/18. +// + +import Firebase + +protocol CreateChatRoomModelProtocol { + var presenter: CreateChatRoomModelOutput! { get set } + var selectedUsersArray: [User] { get set } + var searchedUsersArray: [User] { get set } + + func isContaintsUser(user: User) -> Bool + func searchUser(searchText: String) + func removeSelectedUserFromSelectedUserArray(user: User) + func appendUserToSelectedUserArray(user: User) + func removeSelectedUsersArray(index: Int) -> [User] + func createChatRoom() +} + +protocol CreateChatRoomModelOutput: class { + func successRemoveSelectedUser() + func successAppendUser() + + func successCreateChatRoom() + + func successSearchUser() +} + +final class CreateChatRoomModel: CreateChatRoomModelProtocol { + weak var presenter: CreateChatRoomModelOutput! + private var firestore: Firestore! + var selectedUsersArray: [User] = Array() + var searchedUsersArray: [User] = Array() + + init() { + self.firestore = Firestore.firestore() + let settings = FirestoreSettings() + self.firestore.settings = settings + } + + /// ユーザをFirestoreから検索する関数 + /// - Parameter searchText: 検索するユーザ名 + func searchUser(searchText: String) { + self.firestore.collection("message/v1/users").whereField("displayName", isEqualTo: searchText).getDocuments { [weak self] (documentSnapshot, error) in + if let error = error { + print("Error: \(error.localizedDescription)") + return + } + + guard let documents = documentSnapshot?.documents else { + print("The document doesn't exist.") + return + } + + let searchedUsers = documents.compactMap { queryDocumentSnapshot -> User? in + return try? queryDocumentSnapshot.data(as: User.self) + } + + self?.searchedUsersArray = searchedUsers + self?.presenter.successSearchUser() + } + } + + /// ユーザが既に選択されているかを返す + /// - Parameter user: 検索するユーザ + /// - Returns: 検索結果 + func isContaintsUser(user: User) -> Bool { + if self.selectedUsersArray.filter({ $0.id == user.id ?? "" }).isEmpty { return false } + return true + } + + func removeSelectedUserFromSelectedUserArray(user: User) { + self.selectedUsersArray = self.selectedUsersArray.filter({ $0.id != user.id }) + + self.presenter.successRemoveSelectedUser() + } + + func appendUserToSelectedUserArray(user: User) { + self.selectedUsersArray.append(user) + self.presenter.successAppendUser() + } + + /// `selectedUsersArray`からあるインデックスを削除する + /// - Parameter index: 削除する配列番号 + /// - Returns: 削除した後の`selectedUsersArray` + func removeSelectedUsersArray(index: Int) -> [User] { + self.selectedUsersArray.remove(at: index) + return self.selectedUsersArray + } + + func createChatRoom() { + //TODO:- 以下2行は自分自身の情報にする。Auth()から取得する。 + let uid = "J5AH7imn7esru3RKZPu6" + let name = "Bob" + let memberIDs = [uid] + self.selectedUsersArray.compactMap { $0.id } + let roomName = name + self.selectedUsersArray.compactMap { $0.displayName }.joined(separator: ",") + + //TODO:- `thumbnailImageURL`を決めること + let room = Room(name: roomName, thumbnailImageURL: nil, members: memberIDs, message: "") + let roomData: [String: Any] + + let roomReference = self.firestore.collection("message").document("v1").collection("rooms").document() + let batch = self.firestore.batch() + + do { + roomData = try Firestore.Encoder().encode(room) + } catch let error { + print("Error: \(error.localizedDescription)") + return + } + + batch.setData(roomData, forDocument: roomReference) + + room.members.forEach { memberID in + batch.setData([:], forDocument: roomReference.collection("members").document(memberID)) + } + + batch.commit { error in + if let error = error { + print("Error: \(error.localizedDescription)") + return + } + self.presenter.successCreateChatRoom() + } + } +} diff --git a/chat-iOS/Views/CreateChatRoom/CreateChatRoomViewPresenter.swift b/chat-iOS/Views/CreateChatRoom/CreateChatRoomViewPresenter.swift new file mode 100644 index 0000000..43bc8df --- /dev/null +++ b/chat-iOS/Views/CreateChatRoom/CreateChatRoomViewPresenter.swift @@ -0,0 +1,115 @@ +// +// CreateChatRoomViewPresenter.swift +// chat-iOS +// +// Created by jun on 2020/07/18. +// + +protocol CreateChatRoomViewPresenterProtocol { + var view: CreateChatRoomViewPresenterOutput! { get set } + var numberOfSearchedUsers: Int { get } + var numberOfSelectedUsers: Int { get } + var searchedUsers: [User] { get } + var selectedUsers: [User] { get } + + func isSelected(user: User) -> Bool + + func didSelectedSerchUserTableview(selectedUser: User) + func didTapSelectedUserCollectionViewCellDeleteUserButton(index: Int) + + func didTapStopCreateRoomButton() + func didTapCreateRoomutton() + + func didSearchBarSearchButtonClicked(searchText: String) +} + +protocol CreateChatRoomViewPresenterOutput: class { + func reloadSerchUserTableview() + func reloadSelectedUserCollectionView() + + func hiddenSelectedUsersCollectionView() + func dismissCreateChatRoomVC() + func clearSearchUserTableView() + + func startActivityIndicator() + func stopActivityIndicator() +} + +final class CreateChatRoomViewPresenter: CreateChatRoomViewPresenterProtocol, CreateChatRoomModelOutput { + weak var view: CreateChatRoomViewPresenterOutput! + private var model: CreateChatRoomModelProtocol + + var numberOfSearchedUsers: Int { + return self.model.searchedUsersArray.count + } + + var numberOfSelectedUsers: Int { + return model.selectedUsersArray.count + } + + var searchedUsers: [User] { + return model.searchedUsersArray + } + + var selectedUsers: [User] { + return model.selectedUsersArray + } + + init(model: CreateChatRoomModelProtocol) { + self.model = model + self.model.presenter = self + } + + func didSelectedSerchUserTableview(selectedUser: User) { + if self.model.isContaintsUser(user: selectedUser) { + self.model.removeSelectedUserFromSelectedUserArray(user: selectedUser) + return + } + self.model.appendUserToSelectedUserArray(user: selectedUser) + } + + func didTapSelectedUserCollectionViewCellDeleteUserButton(index: Int) { + let updatedSelectedUsersArray = self.model.removeSelectedUsersArray(index: index) + + self.view.reloadSelectedUserCollectionView() + self.view.reloadSerchUserTableview() + if updatedSelectedUsersArray.isEmpty { self.view.hiddenSelectedUsersCollectionView()} + } + + func didTapStopCreateRoomButton() { + self.view.dismissCreateChatRoomVC() + } + + func didTapCreateRoomutton() { + guard !self.selectedUsers.isEmpty else { return } + self.model.createChatRoom() + } + + func didSearchBarSearchButtonClicked(searchText: String) { + self.view.clearSearchUserTableView() + self.view.startActivityIndicator() + self.model.searchUser(searchText: searchText) + } + + func isSelected(user: User) -> Bool { return self.selectedUsers.firstIndex { user.id == $0.id } != nil } + + func successSearchUser() { + self.view.reloadSerchUserTableview() + self.view.stopActivityIndicator() + } + + func successRemoveSelectedUser() { + self.view.reloadSelectedUserCollectionView() + self.view.reloadSerchUserTableview() + if self.selectedUsers.isEmpty { self.view.hiddenSelectedUsersCollectionView()} + } + + func successAppendUser() { + self.view.reloadSelectedUserCollectionView() + self.view.reloadSerchUserTableview() + } + + func successCreateChatRoom() { + self.view.dismissCreateChatRoomVC() + } +} diff --git a/chat-iOS/Views/CreateChatRoom/SearchUserTableviewCell.swift b/chat-iOS/Views/CreateChatRoom/SearchUserTableviewCell.swift new file mode 100644 index 0000000..9ebf767 --- /dev/null +++ b/chat-iOS/Views/CreateChatRoom/SearchUserTableviewCell.swift @@ -0,0 +1,55 @@ +// +// SearchUserTableviewCell.swift +// chat-iOS +// +// Created by jun on 2020/07/18. +// + +import UIKit + +class SearchUserTableviewCell: UITableViewCell { + + @IBOutlet weak var radioImageView: UIImageView! + @IBOutlet weak var profileImageView: UIImageView! + @IBOutlet weak var userNameLabel: UILabel! + + override func awakeFromNib() { + super.awakeFromNib() + + self.setupRadioImageView() + self.setupProfileImageView() + self.setupUserNameLabel() + } + + private func setupRadioImageView() { + self.radioImageView.contentMode = .scaleAspectFill + self.radioImageView.layer.cornerRadius = self.radioImageView.frame.width / 2 + self.radioImageView.layer.masksToBounds = true + } + + private func setupProfileImageView() { + self.profileImageView.layer.cornerRadius = self.profileImageView.frame.width / 2 + self.profileImageView.layer.masksToBounds = true + } + + private func setupUserNameLabel() { + self.userNameLabel.adjustsFontSizeToFitWidth = true + self.userNameLabel.minimumScaleFactor = 0.4 + } + + func configure(with user: User, isSelected: Bool) { + self.userNameLabel.text = user.displayName + + if #available(iOS 13.0, *) { + radioImageView.image = isSelected ? UIImage(systemName: "checkmark.seal.fill") : UIImage(systemName: "checkmark.seal") + radioImageView.tintColor = isSelected ? .systemGreen : .systemGray + } else { + //TODO:- iOS12以下の画像を用意すること + } + } + + override func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + } +} diff --git a/chat-iOS/Views/CreateChatRoom/SelectedUserCollectionViewCell.swift b/chat-iOS/Views/CreateChatRoom/SelectedUserCollectionViewCell.swift new file mode 100644 index 0000000..472beb0 --- /dev/null +++ b/chat-iOS/Views/CreateChatRoom/SelectedUserCollectionViewCell.swift @@ -0,0 +1,49 @@ +// +// SelectedUserCollectionViewCell.swift +// chat-iOS +// +// Created by jun on 2020/07/18. +// + +import UIKit + +class SelectedUserCollectionViewCell: UICollectionViewCell { + @IBOutlet weak var profileImageView: UIImageView! + @IBOutlet weak var userNameLabel: UILabel! + @IBOutlet weak var deleteUserButton: UIButton! + + var deleteUserButtonAction: (() -> Void)? + + override func awakeFromNib() { + super.awakeFromNib() + + self.setupProfileImageView() + self.setupUserNameLabel() + self.setupDeleteUserButton() + } + + private func setupProfileImageView() { + self.profileImageView.layer.cornerRadius = self.profileImageView.frame.width / 2 + self.profileImageView.layer.masksToBounds = true + } + + private func setupUserNameLabel() { + self.userNameLabel.adjustsFontSizeToFitWidth = true + self.userNameLabel.minimumScaleFactor = 0.4 + } + + private func setupDeleteUserButton() { + self.deleteUserButton.layer.cornerRadius = self.deleteUserButton.frame.width / 2 + self.deleteUserButton.layer.masksToBounds = true + } + + func configure(with user: User) { + self.userNameLabel.text = user.displayName + } + + /// ボタンがタプされた時にクロージャで削除する + /// - Parameter sender: UIButton + @IBAction func tapDeleteUserButton(_ sender: Any) { + self.deleteUserButtonAction?() + } +} diff --git a/chat-iOS/Views/Storyboards/CreateChatRoom.storyboard b/chat-iOS/Views/Storyboards/CreateChatRoom.storyboard new file mode 100644 index 0000000..7212d8c --- /dev/null +++ b/chat-iOS/Views/Storyboards/CreateChatRoom.storyboard @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +