diff --git a/Example/ChatExample.xcodeproj/project.pbxproj b/Example/ChatExample.xcodeproj/project.pbxproj index 038eecfc..5754fcdd 100644 --- a/Example/ChatExample.xcodeproj/project.pbxproj +++ b/Example/ChatExample.xcodeproj/project.pbxproj @@ -7,7 +7,9 @@ objects = { /* Begin PBXBuildFile section */ - 1F16CA6DDBA8B2AED7933182 /* Pods_ChatExample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C5667D3D8E25D4659A3133C2 /* Pods_ChatExample.framework */; }; + 1C5433DD24C389C300A5383B /* MessagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C5433DC24C389C300A5383B /* MessagesView.swift */; }; + 1C5433DF24C38DBF00A5383B /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1C5433DE24C38DBF00A5383B /* SwiftUI.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; + 1C5433E224C3A33600A5383B /* SwiftUIExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C5433E124C3A33600A5383B /* SwiftUIExampleView.swift */; }; 383B9EB32172A1C4008AB91A /* MockUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 383B9EB22172A1C4008AB91A /* MockUser.swift */; }; 385C2922211FF32E0010B4BA /* CustomCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385C2920211FF32E0010B4BA /* CustomCell.swift */; }; 385C2923211FF32E0010B4BA /* TableViewCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385C2921211FF32E0010B4BA /* TableViewCells.swift */; }; @@ -72,10 +74,11 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 10FACBA391F222F83B0D0F14 /* Pods_ChatExampleUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ChatExampleUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 14FD0CD310DC61B22EE2F774 /* Pods-ChatExampleTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ChatExampleTests.debug.xcconfig"; path = "Target Support Files/Pods-ChatExampleTests/Pods-ChatExampleTests.debug.xcconfig"; sourceTree = ""; }; - 15E7C4C168DF654D009EE593 /* Pods-ChatExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ChatExample.release.xcconfig"; path = "Target Support Files/Pods-ChatExample/Pods-ChatExample.release.xcconfig"; sourceTree = ""; }; - 19FCAA41E5FB273F95706474 /* Pods-ChatExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ChatExample.debug.xcconfig"; path = "Target Support Files/Pods-ChatExample/Pods-ChatExample.debug.xcconfig"; sourceTree = ""; }; + 0364943D08CDBE656E6F6DF8 /* Pods-ChatExampleTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ChatExampleTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ChatExampleTests/Pods-ChatExampleTests.debug.xcconfig"; sourceTree = ""; }; + 1C5433DC24C389C300A5383B /* MessagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesView.swift; sourceTree = ""; }; + 1C5433DE24C38DBF00A5383B /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; + 1C5433E124C3A33600A5383B /* SwiftUIExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIExampleView.swift; sourceTree = ""; }; + 2AC6E3F5C11E39F57598DBE6 /* Pods_ChatExampleUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ChatExampleUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 383B9EB22172A1C4008AB91A /* MockUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUser.swift; sourceTree = ""; }; 385C2920211FF32E0010B4BA /* CustomCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomCell.swift; sourceTree = ""; }; 385C2921211FF32E0010B4BA /* TableViewCells.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableViewCells.swift; sourceTree = ""; }; @@ -123,7 +126,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 1F16CA6DDBA8B2AED7933182 /* Pods_ChatExample.framework in Frameworks */, + 1C5433DF24C38DBF00A5383B /* SwiftUI.framework in Frameworks */, + C7CA53A1B85256A5097E7DC7 /* Pods_ChatExample.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -146,19 +150,19 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 13FCBEED2424D36900CB489F /* Resources */ = { + 1C5433E024C3A32400A5383B /* SwiftUI */ = { isa = PBXGroup; children = ( - 882B5E7F1CF7D53600B6E160 /* Info.plist */, - 882B5E791CF7D53600B6E160 /* Assets.xcassets */, - 882B5E7A1CF7D53600B6E160 /* LaunchScreen.storyboard */, + 1C5433DC24C389C300A5383B /* MessagesView.swift */, + 1C5433E124C3A33600A5383B /* SwiftUIExampleView.swift */, ); - path = Resources; + path = SwiftUI; sourceTree = ""; }; 385C2924211FF3310010B4BA /* Views */ = { isa = PBXGroup; children = ( + 1C5433E024C3A32400A5383B /* SwiftUI */, 385C2920211FF32E0010B4BA /* CustomCell.swift */, 385C2921211FF32E0010B4BA /* TableViewCells.swift */, ); @@ -309,9 +313,10 @@ 955AE3B8EFF8FDB8F572D3F7 /* Frameworks */ = { isa = PBXGroup; children = ( - C5667D3D8E25D4659A3133C2 /* Pods_ChatExample.framework */, - 4E9F9C2F0EDEAB72320FDD25 /* Pods_ChatExampleTests.framework */, - 10FACBA391F222F83B0D0F14 /* Pods_ChatExampleUITests.framework */, + 1C5433DE24C38DBF00A5383B /* SwiftUI.framework */, + 3B316705C4717C3B4C916D62 /* Pods_ChatExample.framework */, + 56F0AC85B38034EC92CCBC7D /* Pods_ChatExampleTests.framework */, + 2AC6E3F5C11E39F57598DBE6 /* Pods_ChatExampleUITests.framework */, ); name = Frameworks; sourceTree = ""; @@ -566,6 +571,7 @@ files = ( 385C2922211FF32E0010B4BA /* CustomCell.swift in Sources */, 385C2946211FF38F0010B4BA /* LaunchViewController.swift in Sources */, + 1C5433E224C3A33600A5383B /* SwiftUIExampleView.swift in Sources */, 385C2939211FF37B0010B4BA /* Lorem.swift in Sources */, 385C2928211FF33B0010B4BA /* MockMessage.swift in Sources */, 63ECDABE24182A470016C349 /* MessageSubviewViewController.swift in Sources */, @@ -583,6 +589,7 @@ 385C2943211FF38F0010B4BA /* SettingsViewController.swift in Sources */, 385C2927211FF33B0010B4BA /* MockSocket.swift in Sources */, 385C2947211FF38F0010B4BA /* BasicExampleViewController.swift in Sources */, + 1C5433DD24C389C300A5383B /* MessagesView.swift in Sources */, 882B5E811CF7D53600B6E160 /* AppDelegate.swift in Sources */, 385C292D211FF3520010B4BA /* CustomMessageFlowLayout.swift in Sources */, 38CCCC592258419300DD5482 /* AutocompleteExampleViewController.swift in Sources */, diff --git a/Example/Sources/Data Generation/SampleData.swift b/Example/Sources/Data Generation/SampleData.swift index 1556fe14..e413f3be 100644 --- a/Example/Sources/Data Generation/SampleData.swift +++ b/Example/Sources/Data Generation/SampleData.swift @@ -223,6 +223,21 @@ final internal class SampleData { completion(messages) } + func getMessages(count: Int) -> [MockMessage] { + var messages: [MockMessage] = [] + // Disable Custom Messages + UserDefaults.standard.set(false, forKey: "Custom Messages") + for _ in 0.. Void) { var messages: [MockMessage] = [] // Enable Custom Messages diff --git a/Example/Sources/View Controllers/LaunchViewController.swift b/Example/Sources/View Controllers/LaunchViewController.swift index fa0f46a0..55d110d8 100644 --- a/Example/Sources/View Controllers/LaunchViewController.swift +++ b/Example/Sources/View Controllers/LaunchViewController.swift @@ -25,6 +25,7 @@ import UIKit import MessageKit import SafariServices +import SwiftUI final internal class LaunchViewController: UITableViewController { @@ -32,7 +33,7 @@ final internal class LaunchViewController: UITableViewController { return .lightContent } - let cells = ["Basic Example", "Advanced Example", "Autocomplete Example", "Embedded Example", "Subview Example", "Settings", "Source Code", "Contributors"] + let cells = ["Basic Example", "Advanced Example", "Autocomplete Example", "Embedded Example", "Subview Example", "SwiftUI Example", "Settings", "Source Code", "Contributors"] // MARK: - View Life Cycle @@ -86,9 +87,11 @@ final internal class LaunchViewController: UITableViewController { let detailViewController = NavigationController(rootViewController: viewController) splitViewController?.showDetailViewController(detailViewController, sender: self) case "Embedded Example": - let viewController = MessageContainerController() - let detailViewController = NavigationController(rootViewController: viewController) - splitViewController?.showDetailViewController(detailViewController, sender: self) + navigationController?.pushViewController(MessageContainerController(), animated: true) + case "SwiftUI Example": + if #available(iOS 13, *) { + navigationController?.pushViewController(UIHostingController(rootView: SwiftUIExampleView()), animated: true) + } case "Settings": let viewController = SettingsViewController() let detailViewController = NavigationController(rootViewController: viewController) diff --git a/Example/Sources/Views/SwiftUI/MessagesView.swift b/Example/Sources/Views/SwiftUI/MessagesView.swift new file mode 100644 index 00000000..4247ce2f --- /dev/null +++ b/Example/Sources/Views/SwiftUI/MessagesView.swift @@ -0,0 +1,122 @@ +// +// MessagesView.swift +// ChatExample +// +// Created by Kino Roy on 2020-07-18. +// Copyright © 2020 MessageKit. All rights reserved. +// + +import SwiftUI +import MessageKit +import InputBarAccessoryView + +final class MessageSwiftUIVC: MessagesViewController { + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + // Because SwiftUI wont automatically make our controller the first responder, we need to do it on viewDidAppear + becomeFirstResponder() + messagesCollectionView.scrollToBottom(animated: true) + } +} + +@available(iOS 13.0, *) +struct MessagesView: UIViewControllerRepresentable { + + @State var initialized = false + @Binding var messages: [MessageType] + + func makeUIViewController(context: Context) -> MessagesViewController { + let messagesVC = MessageSwiftUIVC() + + messagesVC.messagesCollectionView.messagesDisplayDelegate = context.coordinator + messagesVC.messagesCollectionView.messagesLayoutDelegate = context.coordinator + messagesVC.messagesCollectionView.messagesDataSource = context.coordinator + messagesVC.messageInputBar.delegate = context.coordinator + messagesVC.scrollsToBottomOnKeyboardBeginsEditing = true // default false + messagesVC.maintainPositionOnKeyboardFrameChanged = true // default false + + return messagesVC + } + + func updateUIViewController(_ uiViewController: MessagesViewController, context: Context) { + uiViewController.messagesCollectionView.reloadData() + scrollToBottom(uiViewController) + } + + private func scrollToBottom(_ uiViewController: MessagesViewController) { + DispatchQueue.main.async { + // The initialized state variable allows us to start at the bottom with the initial messages without seeing the inital scroll flash by + uiViewController.messagesCollectionView.scrollToBottom(animated: self.initialized) + self.initialized = true + } + } + + func makeCoordinator() -> Coordinator { + return Coordinator(messages: $messages) + } + + final class Coordinator { + + let formatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .medium + return formatter + }() + + var messages: Binding<[MessageType]> + init(messages: Binding<[MessageType]>) { + self.messages = messages + } + + } + +} + +@available(iOS 13.0, *) +extension MessagesView.Coordinator: MessagesDataSource { + func currentSender() -> SenderType { + return SampleData.shared.currentSender + } + + func messageForItem(at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageType { + return messages.wrappedValue[indexPath.section] + } + + func numberOfSections(in messagesCollectionView: MessagesCollectionView) -> Int { + return messages.wrappedValue.count + } + + func messageTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { + let name = message.sender.displayName + return NSAttributedString(string: name, attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption1)]) + } + + func messageBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { + let dateString = formatter.string(from: message.sentDate) + return NSAttributedString(string: dateString, attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption2)]) + } +} + +@available(iOS 13.0, *) +extension MessagesView.Coordinator: InputBarAccessoryViewDelegate { + func inputBar(_ inputBar: InputBarAccessoryView, didPressSendButtonWith text: String) { + let message = MockMessage(text: text, user: SampleData.shared.currentSender, messageId: UUID().uuidString, date: Date()) + messages.wrappedValue.append(message) + inputBar.inputTextView.text = "" + } +} + +@available(iOS 13.0, *) +extension MessagesView.Coordinator: MessagesLayoutDelegate, MessagesDisplayDelegate { + func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) { + let avatar = SampleData.shared.getAvatarFor(sender: message.sender) + avatarView.set(avatar: avatar) + } + func messageTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat { + return 20 + } + + func messageBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat { + return 16 + } +} diff --git a/Example/Sources/Views/SwiftUI/SwiftUIExampleView.swift b/Example/Sources/Views/SwiftUI/SwiftUIExampleView.swift new file mode 100644 index 00000000..41d3b0f2 --- /dev/null +++ b/Example/Sources/Views/SwiftUI/SwiftUIExampleView.swift @@ -0,0 +1,43 @@ +// +// ChatView.swift +// ChatExample +// +// Created by Kino Roy on 2020-07-18. +// Copyright © 2020 MessageKit. All rights reserved. +// + +import SwiftUI +import MessageKit + +@available(iOS 13.0, *) +struct SwiftUIExampleView: View { + + @State var messages: [MessageType] = SampleData.shared.getMessages(count: 20) + + var body: some View { + MessagesView(messages: $messages).onAppear { + self.connectToMessageSocket() + }.onDisappear { + self.cleanupSocket() + } + .navigationBarTitle("SwiftUI Example", displayMode: .inline) + } + + private func connectToMessageSocket() { + MockSocket.shared.connect(with: [SampleData.shared.nathan, SampleData.shared.wu]).onNewMessage { message in + self.messages.append(message) + } + } + + private func cleanupSocket() { + MockSocket.shared.disconnect() + } + +} + +@available(iOS 13.0, *) +struct SwiftUIExampleView_Previews: PreviewProvider { + static var previews: some View { + SwiftUIExampleView() + } +} diff --git a/GitHubActions/build.sh b/GitHubActions/build.sh index 3f998f9a..7bcd40ea 100755 --- a/GitHubActions/build.sh +++ b/GitHubActions/build.sh @@ -53,7 +53,7 @@ if [ "$MODE" = "example" -o "$MODE" = "all" ]; then gem install bundler bundle check || bundle install bundle exec pod install - set -o pipefail && xcodebuild build analyze -workspace ChatExample.xcworkspace -scheme ChatExample -destination "platform=iOS Simulator,name=iPhone 11 Pro" ONLY_ACTIVE_ARCH=NO CODE_SIGNING_REQUIRED=NO | xcpretty -c + set -o pipefail && xcodebuild build analyze -workspace ChatExample.xcworkspace -scheme ChatExample -destination "platform=iOS Simulator,name=iPhone 11 Pro" CODE_SIGNING_REQUIRED=NO | xcpretty -c success="1" fi diff --git a/README.md b/README.md index 9e3b6d0f..968b6f0a 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ The `MessageInputBar`, derrived from [InputBarAccessoryView](https://github.com/ Please have a look at the [Quick Start guide](https://github.com/MessageKit/MessageKit/blob/master/Documentation/QuickStart.md) and the [FAQs](https://github.com/MessageKit/MessageKit/blob/master/Documentation/FAQs.md). -We recommend you start by looking at the [Example](https://github.com/MessageKit/MessageKit/tree/master/Example) project or write a question with the "messagekit" tag on [Stack Overflow](https://stackoverflow.com/questions/tagged/messagekit). You can also look at previous issues here on GitHib with the **"Question"** tag. +We recommend you start by looking at the [Example](https://github.com/MessageKit/MessageKit/tree/master/Example) project or write a question with the "messagekit" tag on [Stack Overflow](https://stackoverflow.com/questions/tagged/messagekit). You can also look at previous issues here on GitHub with the **"Question"** tag. For more on how to use the MessageInputBar, see the dependency it is based on [InputBarAccessoryView](https://github.com/nathantannar4/InputBarAccessoryView). You can also see this [short guide]([https://github.com/MessageKit/MessageKit/blob/master/Documentation/MessageInputBar.md) @@ -163,8 +163,9 @@ Great! Look over these things first. - Download the project and check out the current code base. Suggest any improvements by opening a new issue. - Check out the [What's Next](#whats-next) section :point_down: to see where we are headed. - Check [StackOverflow](https://stackoverflow.com/questions/tagged/messagekit) -- Install [SwiftLint](https://github.com/realm/SwiftLint) too keep yourself in :neckbeard: style. -- Be kind and helpful. +- Install [SwiftLint](https://github.com/realm/SwiftLint) to keep yourself in :neckbeard: style. +- Be kind and helpful. + ## What's Next? @@ -190,6 +191,7 @@ Add your app to the list of apps using this library and make a pull request. - [Studievenn](https://studievenn.no) - [SmooveText](https://apps.apple.com/np/app/smoove-text/id1362792811) - [COYO Engage](https://apps.apple.com/app/coyo-engage/id1341588804) +- [HitchPin](https://www.hitchpin.com) _Please provide attribution, it is greatly appreciated._