diff --git a/.github/workflows/ci_pr_example.yml b/.github/workflows/ci_pr_example.yml index 437310337..d98c63fda 100644 --- a/.github/workflows/ci_pr_example.yml +++ b/.github/workflows/ci_pr_example.yml @@ -10,7 +10,7 @@ concurrency: jobs: tests: name: Build Example app - runs-on: macos-14 + runs-on: macos-15 steps: - name: Checkout the Git repository uses: actions/checkout@v4 diff --git a/.github/workflows/ci_pr_framework.yml b/.github/workflows/ci_pr_framework.yml index da5365c13..08bd9b031 100644 --- a/.github/workflows/ci_pr_framework.yml +++ b/.github/workflows/ci_pr_framework.yml @@ -10,7 +10,7 @@ concurrency: jobs: tests: name: Build Framework - runs-on: macos-14 + runs-on: macos-15 steps: - name: Checkout the Git repository uses: actions/checkout@v4 diff --git a/.github/workflows/ci_pr_tests.yml b/.github/workflows/ci_pr_tests.yml index 0c1058452..cd53f5472 100644 --- a/.github/workflows/ci_pr_tests.yml +++ b/.github/workflows/ci_pr_tests.yml @@ -10,7 +10,7 @@ concurrency: jobs: tests: name: Run Tests - runs-on: macos-14 + runs-on: macos-15 steps: - name: Checkout the Git repository uses: actions/checkout@v4 diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml index 9648963dd..a05b6143f 100644 --- a/.github/workflows/deploy_docs.yml +++ b/.github/workflows/deploy_docs.yml @@ -15,7 +15,7 @@ concurrency: jobs: build_docs: - runs-on: macos-14 + runs-on: macos-15 steps: - name: Checkout 🛎️ uses: actions/checkout@v4 @@ -33,7 +33,7 @@ jobs: --hosting-base-path MessageKit/InputBarAccessoryView \ --output-path docs/InputBarAccessoryView - name: Deploy to GitHub Pages - uses: peaceiris/actions-gh-pages@v3 + uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./docs diff --git a/Example/ChatExample.xcodeproj/project.pbxproj b/Example/ChatExample.xcodeproj/project.pbxproj index 10cd6690b..49465a9fd 100644 --- a/Example/ChatExample.xcodeproj/project.pbxproj +++ b/Example/ChatExample.xcodeproj/project.pbxproj @@ -673,13 +673,13 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; INFOPLIST_FILE = "$(SRCROOT)/Sources/Resources/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.messagekit.ChatExample; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; }; name = Debug; }; @@ -688,6 +688,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; INFOPLIST_FILE = "$(SRCROOT)/Sources/Resources/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -696,7 +697,6 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; - SWIFT_VERSION = 5.0; }; name = Release; }; @@ -705,6 +705,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; INFOPLIST_FILE = Tests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -712,7 +713,6 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.hexedbits.ChatExampleTests; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ChatExample.app/ChatExample"; }; name = Debug; @@ -722,6 +722,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; INFOPLIST_FILE = Tests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -731,7 +732,6 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; - SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ChatExample.app/ChatExample"; }; name = Release; @@ -740,6 +740,7 @@ isa = XCBuildConfiguration; buildSettings = { INFOPLIST_FILE = UITests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -747,7 +748,6 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.hexedbits.ChatExampleUITests; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; TEST_TARGET_NAME = ChatExample; }; name = Debug; @@ -756,6 +756,7 @@ isa = XCBuildConfiguration; buildSettings = { INFOPLIST_FILE = UITests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -765,7 +766,6 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; - SWIFT_VERSION = 5.0; TEST_TARGET_NAME = ChatExample; }; name = Release; @@ -817,7 +817,7 @@ repositoryURL = "https://github.com/onevcat/Kingfisher"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 5.15.8; + minimumVersion = 8.0.0; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Example/Sources/AppDelegate.swift b/Example/Sources/AppDelegate.swift index 3cb9f9d4a..604e3eb43 100644 --- a/Example/Sources/AppDelegate.swift +++ b/Example/Sources/AppDelegate.swift @@ -22,7 +22,7 @@ import UIKit -@UIApplicationMain +@main final internal class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? @@ -35,7 +35,7 @@ final internal class AppDelegate: UIResponder, UIApplicationDelegate { UIViewController() ] : [masterViewController] - splitViewController.preferredDisplayMode = .allVisible + splitViewController.preferredDisplayMode = UISplitViewController.DisplayMode.oneBesideSecondary masterViewController.navigationItem.largeTitleDisplayMode = .never window = UIWindow(frame: UIScreen.main.bounds) diff --git a/Example/Sources/AudioController/BasicAudioController.swift b/Example/Sources/AudioController/BasicAudioController.swift index 8bdd84d8b..9f239d3d8 100644 --- a/Example/Sources/AudioController/BasicAudioController.swift +++ b/Example/Sources/AudioController/BasicAudioController.swift @@ -42,7 +42,8 @@ public enum PlayerState { /// The `BasicAudioController` update UI for current audio cell that is playing a sound /// and also creates and manage an `AVAudioPlayer` states, play, pause and stop. -open class BasicAudioController: NSObject, AVAudioPlayerDelegate { +@MainActor +open class BasicAudioController: NSObject, @preconcurrency AVAudioPlayerDelegate { // MARK: Lifecycle // MARK: - Init Methods diff --git a/Example/Sources/Models/MockSocket.swift b/Example/Sources/Models/MockSocket.swift index d48cacd63..41c9f1b11 100644 --- a/Example/Sources/Models/MockSocket.swift +++ b/Example/Sources/Models/MockSocket.swift @@ -23,6 +23,7 @@ import MessageKit import UIKit +@MainActor final class MockSocket { // MARK: Lifecycle diff --git a/Example/Sources/Resources/Assets.xcassets/bobbly.imageset/Group 1272628320.png b/Example/Sources/Resources/Assets.xcassets/bobbly.imageset/Group 1272628320.png index faaad6d77..be5d8ca35 100644 Binary files a/Example/Sources/Resources/Assets.xcassets/bobbly.imageset/Group 1272628320.png and b/Example/Sources/Resources/Assets.xcassets/bobbly.imageset/Group 1272628320.png differ diff --git a/Example/Sources/Resources/Assets.xcassets/bobbly.imageset/Group 1272628320@2x.png b/Example/Sources/Resources/Assets.xcassets/bobbly.imageset/Group 1272628320@2x.png index bdbde8f5a..11eb26a18 100644 Binary files a/Example/Sources/Resources/Assets.xcassets/bobbly.imageset/Group 1272628320@2x.png and b/Example/Sources/Resources/Assets.xcassets/bobbly.imageset/Group 1272628320@2x.png differ diff --git a/Example/Sources/Resources/Assets.xcassets/bobbly.imageset/Group 1272628320@3x.png b/Example/Sources/Resources/Assets.xcassets/bobbly.imageset/Group 1272628320@3x.png index 156e9f5c5..1bb9c5579 100644 Binary files a/Example/Sources/Resources/Assets.xcassets/bobbly.imageset/Group 1272628320@3x.png and b/Example/Sources/Resources/Assets.xcassets/bobbly.imageset/Group 1272628320@3x.png differ diff --git a/Gemfile.lock b/Gemfile.lock index 7af99e226..57adcd5ab 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -70,13 +70,11 @@ GEM public_suffix (5.0.1) rake (13.0.6) rchardet (1.8.0) - rexml (3.3.6) - strscan + rexml (3.3.9) ruby2_keywords (0.0.5) sawyer (0.9.2) addressable (>= 2.3.5) faraday (>= 0.17.3, < 3) - strscan (3.1.0) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) thor (0.20.3) diff --git a/Makefile b/Makefile index 87ab6715a..56e05c78f 100644 --- a/Makefile +++ b/Makefile @@ -25,15 +25,15 @@ test: @echo "Running MessageKit tests." - @set -o pipefail && xcodebuild test -scheme MessageKit -sdk iphonesimulator -destination "platform=iOS Simulator,name=iPhone 15" | xcpretty -c + @set -o pipefail && xcodebuild test -scheme MessageKit -sdk iphonesimulator -destination "platform=iOS Simulator,name=iPhone 16" | xcpretty -c framework: @echo "Building MessageKit Framework." - @set -o pipefail && xcodebuild build -scheme MessageKit -destination "platform=iOS Simulator,name=iPhone 15" | xcpretty -c + @set -o pipefail && xcodebuild build -scheme MessageKit -destination "platform=iOS Simulator,name=iPhone 16" | xcpretty -c build_example: @echo "Building & testing MessageKit Example app." - @cd Example && set -o pipefail && xcodebuild build analyze -scheme ChatExample -destination "platform=iOS Simulator,name=iPhone 15" CODE_SIGNING_REQUIRED=NO | xcpretty -c + @cd Example && set -o pipefail && xcodebuild build analyze -scheme ChatExample -destination "platform=iOS Simulator,name=iPhone 16" CODE_SIGNING_REQUIRED=NO | xcpretty -c format: @swift package --allow-writing-to-package-directory format-source-code --file . diff --git a/Package.swift b/Package.swift index b0e0f6747..ca0bcbd50 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.10 +// swift-tools-version:6.0 // MIT License // @@ -24,14 +24,12 @@ import PackageDescription let package = Package( name: "MessageKit", - platforms: [.iOS(.v13)], + platforms: [.iOS(.v14)], products: [ - .library(name: "MessageKit", targets: ["MessageKit"]), - .plugin(name: "LintPlugin", targets: ["LintPlugin"]), - .plugin(name: "SwiftFormatPlugin", targets: ["SwiftFormatPlugin"]), + .library(name: "MessageKit", targets: ["MessageKit"]) ], dependencies: [ - .package(url: "https://github.com/nathantannar4/InputBarAccessoryView", .upToNextMajor(from: "6.5.0")), + .package(url: "https://github.com/nathantannar4/InputBarAccessoryView", .upToNextMajor(from: "7.0.0")), ], targets: [ // MARK: - MessageKit @@ -44,45 +42,6 @@ let package = Package( swiftSettings: [SwiftSetting.define("IS_SPM")] ), .testTarget(name: "MessageKitTests", dependencies: ["MessageKit"]), - - // MARK: - Plugins - - .binaryTarget( - name: "LintBinary", - url: "https://github.com/realm/SwiftLint/releases/download/0.47.1/SwiftLintBinary-macos.artifactbundle.zip", - checksum: "82ef90b7d76b02e41edd73423687d9cedf0bb9849dcbedad8df3a461e5a7b555" - ), - .plugin( - name: "LintPlugin", - capability: .buildTool(), - dependencies: ["LintBinary"] - ), - .plugin( - name: "LintCommandPlugin", - capability: .command( - intent: .custom( - verb: "lint", - description: "Lint Swift source files" - ) - ), - dependencies: ["LintBinary"] - ), - - .binaryTarget( - name: "swiftformat", - url: "https://github.com/nicklockwood/SwiftFormat/releases/download/0.49.13/swiftformat.artifactbundle.zip", - checksum: "5ce27780dceee8714b15d53141e6dce1a8d626e970eade3c511c9ef1a0c06f40" - ), - .plugin( - name: "SwiftFormatPlugin", - capability: .command( - intent: .sourceCodeFormatting(), - permissions: [ - .writeToPackageDirectory(reason: "Format Swift source files"), - ] - ), - dependencies: ["swiftformat"] - ), ], - swiftLanguageVersions: [.v5] + swiftLanguageModes: [.v6] ) diff --git a/Plugins/LintCommandPlugin/SwiftLintCommandPlugin.swift b/Plugins/LintCommandPlugin/SwiftLintCommandPlugin.swift deleted file mode 100644 index c75bcf17c..000000000 --- a/Plugins/LintCommandPlugin/SwiftLintCommandPlugin.swift +++ /dev/null @@ -1,54 +0,0 @@ -// MIT License -// -// Copyright (c) 2017-2022 MessageKit -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -import Foundation -import PackagePlugin - -// MARK: - SwiftLintCommandPlugin - -@main -struct SwiftLintCommandPlugin: CommandPlugin { - func performCommand(context: PackagePlugin.PluginContext, arguments _: [String]) async throws { - let swiftLintTool = try context.tool(named: "swiftlint") - let swiftLintPath = URL(fileURLWithPath: swiftLintTool.path.string) - - let swiftLintArgs = [ - "lint", - "--path", context.package.directory.string, - "--config", context.package.directory.string + "/.swiftlint.yml", - "--strict", - ] - - let task = try Process.run(swiftLintPath, arguments: swiftLintArgs) - task.waitUntilExit() - - if task.terminationStatus == 0 || task.terminationStatus == 2 { - // no-op - } else { - throw CommandError.unknownError(exitCode: task.terminationStatus) - } - } -} - -// MARK: - CommandError - -enum CommandError: Error { - case unknownError(exitCode: Int32) -} diff --git a/Plugins/LintPlugin/SwiftLintPlugin.swift b/Plugins/LintPlugin/SwiftLintPlugin.swift deleted file mode 100644 index e88b10abd..000000000 --- a/Plugins/LintPlugin/SwiftLintPlugin.swift +++ /dev/null @@ -1,41 +0,0 @@ -// MIT License -// -// Copyright (c) 2017-2022 MessageKit -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -import PackagePlugin - -@main -struct LintPlugin: BuildToolPlugin { - func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] { - [ - .buildCommand( - displayName: "Linting \(target.name)", - executable: try context.tool(named: "swiftlint").path, - arguments: [ - "lint", - "--in-process-sourcekit", - "--path", - target.directory.string, - "--config", - "\(context.package.directory.string)/.swiftlint.yml", - ], - environment: [:]), - ] - } -} diff --git a/Plugins/SwiftFormatPlugin/SwiftFormatPlugin.swift b/Plugins/SwiftFormatPlugin/SwiftFormatPlugin.swift deleted file mode 100644 index 34b1d70de..000000000 --- a/Plugins/SwiftFormatPlugin/SwiftFormatPlugin.swift +++ /dev/null @@ -1,61 +0,0 @@ -// MIT License -// -// Copyright (c) 2017-2022 MessageKit -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -import Foundation -import PackagePlugin - -// MARK: - SwiftFormatPlugin - -@main -struct SwiftFormatPlugin: CommandPlugin { - func performCommand(context: PackagePlugin.PluginContext, arguments: [String]) async throws { - var argumentExtractor = ArgumentExtractor(arguments) - let files = argumentExtractor.extractOption(named: "file") - - let process = Process() - process.launchPath = try context.tool(named: "swiftformat").path.string - process.arguments = [ - files.first!, - "--swiftversion", - "5.6", - "--cache", - context.pluginWorkDirectory.string + "/swiftformat.cache", - ] - - try process.run() - process.waitUntilExit() - - switch process.terminationStatus { - case EXIT_SUCCESS: - break - case EXIT_FAILURE: - throw CommandError.lintFailure - default: - throw CommandError.unknownError(exitCode: process.terminationStatus) - } - } -} - -// MARK: - CommandError - -enum CommandError: Error { - case lintFailure - case unknownError(exitCode: Int32) -} diff --git a/README.md b/README.md index d34412a50..95b077216 100644 --- a/README.md +++ b/README.md @@ -48,8 +48,10 @@ Older versions of Swift and Xcode don't support MessageKit via SPM. ## Requirements -- **iOS 13** or later -- **Swift 5.5** or later +- **iOS 14** or later +- **Swift 6** or later +- +> For iOS 13 or Swift 5.x please use version 4.3.0 > For iOS 12 or CocoaPods please use version 3.8.0 diff --git a/Sources/Controllers/MessagesViewController+Menu.swift b/Sources/Controllers/MessagesViewController+Menu.swift index d454d3fc2..7aa07a36d 100644 --- a/Sources/Controllers/MessagesViewController+Menu.swift +++ b/Sources/Controllers/MessagesViewController+Menu.swift @@ -36,10 +36,6 @@ extension MessagesViewController { object: nil) } - internal func removeMenuControllerObservers() { - NotificationCenter.default.removeObserver(self, name: UIMenuController.willShowMenuNotification, object: nil) - } - // MARK: Private // MARK: - Helpers diff --git a/Sources/Controllers/MessagesViewController+State.swift b/Sources/Controllers/MessagesViewController+State.swift index d72c3d186..62d2d9cdb 100644 --- a/Sources/Controllers/MessagesViewController+State.swift +++ b/Sources/Controllers/MessagesViewController+State.swift @@ -26,17 +26,18 @@ import InputBarAccessoryView import UIKit extension MessagesViewController { - final class State { - /// Pan gesture for display the date of message by swiping left. - var panGesture: UIPanGestureRecognizer? - var maintainPositionOnInputBarHeightChanged = false - var scrollsToLastItemOnKeyboardBeginsEditing = false + @MainActor + final class State { + /// Pan gesture for display the date of message by swiping left. + var panGesture: UIPanGestureRecognizer? + var maintainPositionOnInputBarHeightChanged = false + var scrollsToLastItemOnKeyboardBeginsEditing = false - let inputContainerView: MessagesInputContainerView = .init() - @Published var inputBarType: MessageInputBarKind = .messageInputBar - let keyboardManager = KeyboardManager() - var disposeBag: Set = .init() - } + let inputContainerView: MessagesInputContainerView = .init() + @Published var inputBarType: MessageInputBarKind = .messageInputBar + let keyboardManager = KeyboardManager() + var disposeBag: Set = .init() + } // MARK: - Getters diff --git a/Sources/Controllers/MessagesViewController.swift b/Sources/Controllers/MessagesViewController.swift index 1046517d5..41965d5a4 100644 --- a/Sources/Controllers/MessagesViewController.swift +++ b/Sources/Controllers/MessagesViewController.swift @@ -31,8 +31,8 @@ open class MessagesViewController: UIViewController, UICollectionViewDelegateFlo // MARK: Lifecycle deinit { - removeMenuControllerObservers() - clearMemoryCache() + NotificationCenter.default.removeObserver(self, name: UIMenuController.willShowMenuNotification, object: nil) + MessageStyle.bubbleImageCache.removeAllObjects() } // MARK: Open diff --git a/Sources/Extensions/Bundle+Extensions.swift b/Sources/Extensions/Bundle+Extensions.swift index bc88f309e..aa4ca7d4f 100644 --- a/Sources/Extensions/Bundle+Extensions.swift +++ b/Sources/Extensions/Bundle+Extensions.swift @@ -24,9 +24,9 @@ import Foundation extension Bundle { #if IS_SPM - internal static var messageKitAssetBundle = Bundle.module + nonisolated internal static let messageKitAssetBundle = Bundle.module #else - internal static var messageKitAssetBundle: Bundle { + nonisolated internal static var messageKitAssetBundle: Bundle { Bundle(for: MessagesViewController.self) } #endif diff --git a/Sources/Extensions/UIColor+Extensions.swift b/Sources/Extensions/UIColor+Extensions.swift index b40233cef..e5df7ad80 100644 --- a/Sources/Extensions/UIColor+Extensions.swift +++ b/Sources/Extensions/UIColor+Extensions.swift @@ -23,6 +23,7 @@ import Foundation import UIKit +@MainActor extension UIColor { // MARK: Internal diff --git a/Sources/Layout/CellSizeCalculator.swift b/Sources/Layout/CellSizeCalculator.swift index 6c153ceb9..85ffbffed 100644 --- a/Sources/Layout/CellSizeCalculator.swift +++ b/Sources/Layout/CellSizeCalculator.swift @@ -24,6 +24,7 @@ import UIKit /// An object is responsible for /// sizing and configuring cells for given `IndexPath`s. +@MainActor open class CellSizeCalculator { // MARK: Lifecycle diff --git a/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift b/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift index d6d69e12c..ff8fefad6 100644 --- a/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift +++ b/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift @@ -23,7 +23,7 @@ import UIKit /// The layout attributes used by a `MessageCollectionViewCell` to layout its subviews. -open class MessagesCollectionViewLayoutAttributes: UICollectionViewLayoutAttributes { +open class MessagesCollectionViewLayoutAttributes: UICollectionViewLayoutAttributes { // MARK: Open // MARK: - Methods @@ -55,36 +55,6 @@ open class MessagesCollectionViewLayoutAttributes: UICollectionViewLayoutAttribu // swiftlint:enable force_cast } - open override func isEqual(_ object: Any?) -> Bool { - // MARK: - LEAVE this as is - if let attributes = object as? MessagesCollectionViewLayoutAttributes { - return super.isEqual(object) && attributes.avatarSize == avatarSize - && attributes.avatarPosition == avatarPosition - && attributes.avatarLeadingTrailingPadding == avatarLeadingTrailingPadding - && attributes.messageContainerSize == messageContainerSize - && attributes.messageContainerPadding == messageContainerPadding - && attributes.messageLabelFont == messageLabelFont - && attributes.messageLabelInsets == messageLabelInsets - && attributes.cellTopLabelAlignment == cellTopLabelAlignment - && attributes.cellTopLabelSize == cellTopLabelSize - && attributes.cellBottomLabelAlignment == cellBottomLabelAlignment - && attributes.cellBottomLabelSize == cellBottomLabelSize - && attributes.messageTimeLabelSize == messageTimeLabelSize - && attributes.messageTopLabelAlignment == messageTopLabelAlignment - && attributes.messageTopLabelSize == messageTopLabelSize - && attributes.messageBottomLabelAlignment == messageBottomLabelAlignment - && attributes.messageBottomLabelSize == messageBottomLabelSize - && attributes.accessoryViewSize == accessoryViewSize - && attributes.accessoryViewPadding == accessoryViewPadding - && attributes.accessoryViewPosition == accessoryViewPosition - && attributes.linkPreviewFonts == linkPreviewFonts - } else { - return false - } - } - - // MARK: Public - // MARK: - Properties public var avatarSize: CGSize = .zero diff --git a/Sources/Models/AccessoryPosition.swift b/Sources/Models/AccessoryPosition.swift index 3fd69bb3a..59c09c5ce 100644 --- a/Sources/Models/AccessoryPosition.swift +++ b/Sources/Models/AccessoryPosition.swift @@ -24,7 +24,7 @@ import Foundation /// Used to determine the `Horizontal` and `Vertical` position of /// an `AccessoryView` in a `MessageCollectionViewCell`. -public enum AccessoryPosition { +public enum AccessoryPosition: Equatable { /// Aligns the `AccessoryView`'s top edge to the cell's top edge. case cellTop diff --git a/Sources/Models/DetectorType.swift b/Sources/Models/DetectorType.swift index d2dd83e9a..7fd0cb228 100644 --- a/Sources/Models/DetectorType.swift +++ b/Sources/Models/DetectorType.swift @@ -22,7 +22,7 @@ import Foundation -public enum DetectorType: Hashable { +public enum DetectorType: Hashable, Sendable { case address case date case phoneNumber @@ -33,8 +33,8 @@ public enum DetectorType: Hashable { // MARK: Public // swiftlint:disable force_try - public static var hashtag = DetectorType.custom(try! NSRegularExpression(pattern: "#[a-zA-Z0-9]{4,}", options: [])) - public static var mention = DetectorType.custom(try! NSRegularExpression(pattern: "@[a-zA-Z0-9]{4,}", options: [])) + public static let hashtag = DetectorType.custom(try! NSRegularExpression(pattern: "#[a-zA-Z0-9]{4,}", options: [])) + public static let mention = DetectorType.custom(try! NSRegularExpression(pattern: "@[a-zA-Z0-9]{4,}", options: [])) /// Simply check if the detector type is a .custom public var isCustom: Bool { diff --git a/Sources/Models/LocationMessageSnapshotOptions.swift b/Sources/Models/LocationMessageSnapshotOptions.swift index bed668432..792a07310 100644 --- a/Sources/Models/LocationMessageSnapshotOptions.swift +++ b/Sources/Models/LocationMessageSnapshotOptions.swift @@ -23,6 +23,7 @@ import MapKit /// An object grouping the settings used by the `MKMapSnapshotter` through the `LocationMessageDisplayDelegate`. +@MainActor public struct LocationMessageSnapshotOptions { // MARK: Lifecycle diff --git a/Sources/Models/MessageKitDateFormatter.swift b/Sources/Models/MessageKitDateFormatter.swift index 7d39a9fa8..c3636034e 100644 --- a/Sources/Models/MessageKitDateFormatter.swift +++ b/Sources/Models/MessageKitDateFormatter.swift @@ -22,7 +22,7 @@ import Foundation -open class MessageKitDateFormatter { +open class MessageKitDateFormatter: @unchecked Sendable { // MARK: Lifecycle // MARK: - Initializer diff --git a/Sources/Models/MessageStyle.swift b/Sources/Models/MessageStyle.swift index fcdbbf60f..ed1dbeba4 100644 --- a/Sources/Models/MessageStyle.swift +++ b/Sources/Models/MessageStyle.swift @@ -44,12 +44,12 @@ public enum MessageStyle { case bottomRight internal var imageOrientation: UIImage.Orientation { - switch self { - case .bottomRight: return .up - case .bottomLeft: return .upMirrored - case .topLeft: return .down - case .topRight: return .downMirrored - } + switch self { + case .bottomRight: return .up + case .bottomLeft: return .upMirrored + case .topLeft: return .down + case .topRight: return .downMirrored + } } } @@ -111,13 +111,13 @@ public enum MessageStyle { return strechAndCache(image: image) } - // MARK: Internal + // MARK: Internal - internal static let bubbleImageCache: NSCache = { - let cache = NSCache() - cache.name = "com.messagekit.MessageKit.bubbleImageCache" - return cache - }() + nonisolated(unsafe) internal static let bubbleImageCache: NSCache = { + let cache = NSCache() + cache.name = "com.messagekit.MessageKit.bubbleImageCache" + return cache + }() // MARK: Private diff --git a/Sources/Protocols/MessagesDataSource.swift b/Sources/Protocols/MessagesDataSource.swift index 7729d898b..831d1cf2f 100644 --- a/Sources/Protocols/MessagesDataSource.swift +++ b/Sources/Protocols/MessagesDataSource.swift @@ -26,6 +26,7 @@ import UIKit /// An object that adopts the `MessagesDataSource` protocol is responsible for providing /// the data required by a `MessagesCollectionView`. +@MainActor public protocol MessagesDataSource: AnyObject { /// The `SenderType` of new messages in the `MessagesCollectionView`. var currentSender: SenderType { get } diff --git a/Sources/Protocols/MessagesDisplayDelegate.swift b/Sources/Protocols/MessagesDisplayDelegate.swift index 2548fde8f..81a5d6722 100644 --- a/Sources/Protocols/MessagesDisplayDelegate.swift +++ b/Sources/Protocols/MessagesDisplayDelegate.swift @@ -27,6 +27,7 @@ import UIKit // MARK: - MessagesDisplayDelegate /// A protocol used by the `MessagesViewController` to customize the appearance of a `MessageContentCell`. +@MainActor public protocol MessagesDisplayDelegate: AnyObject { // MARK: - All Messages diff --git a/Sources/Protocols/MessagesLayoutDelegate.swift b/Sources/Protocols/MessagesLayoutDelegate.swift index b6fafe1c3..015c45c30 100644 --- a/Sources/Protocols/MessagesLayoutDelegate.swift +++ b/Sources/Protocols/MessagesLayoutDelegate.swift @@ -27,6 +27,7 @@ import UIKit /// A protocol used by the `MessagesCollectionViewFlowLayout` object to determine /// the size and layout of a `MessageCollectionViewCell` and its contents. +@MainActor public protocol MessagesLayoutDelegate: AnyObject { /// Specifies the size to use for a header view. /// diff --git a/Sources/Views/Cells/LocationMessageCell.swift b/Sources/Views/Cells/LocationMessageCell.swift index 3f42c7171..44601b1b8 100644 --- a/Sources/Views/Cells/LocationMessageCell.swift +++ b/Sources/Views/Cells/LocationMessageCell.swift @@ -58,6 +58,12 @@ open class LocationMessageCell: MessageContentCell { and messagesCollectionView: MessagesCollectionView) { super.configure(with: message, at: indexPath, and: messagesCollectionView) + + guard case .location(let locationItem) = message.kind else { fatalError("Configuring LocationMessageCell with wrong message kind") } + guard CLLocationCoordinate2DIsValid(locationItem.location.coordinate) else { + return + } + guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else { fatalError(MessageKitError.nilMessagesDisplayDelegate) } @@ -71,8 +77,6 @@ open class LocationMessageCell: MessageContentCell { at: indexPath, in: messagesCollectionView) - guard case .location(let locationItem) = message.kind else { fatalError("Configuring LocationMessageCell with wrong") } - activityIndicator.startAnimating() let snapshotOptions = MKMapSnapshotter.Options() diff --git a/Sources/Views/TypingIndicator.swift b/Sources/Views/TypingIndicator.swift index 208ce337c..355600987 100644 --- a/Sources/Views/TypingIndicator.swift +++ b/Sources/Views/TypingIndicator.swift @@ -87,23 +87,24 @@ open class TypingIndicator: UIView { /// Sets the state of the `TypingIndicator` to animating and applies animation layers open func startAnimating() { - defer { isAnimating = true } - guard !isAnimating else { return } - var delay: TimeInterval = 0 - for dot in dots { - DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in - guard let self = self else { return } - if self.isBounceEnabled { - dot.layer.add(self.initialOffsetAnimationLayer, forKey: AnimationKeys.offset) - let bounceLayer = self.bounceAnimationLayer - bounceLayer.timeOffset = delay + 0.33 - dot.layer.add(bounceLayer, forKey: AnimationKeys.bounce) - } - if self.isFadeEnabled { - dot.layer.add(self.opacityAnimationLayer, forKey: AnimationKeys.opacity) - } - } - delay += 0.33 + defer { isAnimating = true } + guard !isAnimating else { return } + var delay: TimeInterval = 0 + for dot in dots { + let currentDelay = delay + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in + guard let self = self else { return } + if self.isBounceEnabled { + dot.layer.add(self.initialOffsetAnimationLayer, forKey: AnimationKeys.offset) + let bounceLayer = self.bounceAnimationLayer + bounceLayer.timeOffset = currentDelay + 0.33 + dot.layer.add(bounceLayer, forKey: AnimationKeys.bounce) + } + if self.isFadeEnabled { + dot.layer.add(self.opacityAnimationLayer, forKey: AnimationKeys.opacity) + } + } + delay += 0.33 } } diff --git a/Tests/MessageKitTests/Controllers Test/MessageLabelTests.swift b/Tests/MessageKitTests/Controllers Test/MessageLabelTests.swift index 4d1264768..78cae636b 100644 --- a/Tests/MessageKitTests/Controllers Test/MessageLabelTests.swift +++ b/Tests/MessageKitTests/Controllers Test/MessageLabelTests.swift @@ -26,6 +26,7 @@ import XCTest // MARK: - MessageLabelTests +@MainActor final class MessageLabelTests: XCTestCase { // MARK: Internal diff --git a/Tests/MessageKitTests/Controllers Test/MessagesViewControllerTests.swift b/Tests/MessageKitTests/Controllers Test/MessagesViewControllerTests.swift index 858764783..417bcea11 100644 --- a/Tests/MessageKitTests/Controllers Test/MessagesViewControllerTests.swift +++ b/Tests/MessageKitTests/Controllers Test/MessagesViewControllerTests.swift @@ -26,264 +26,264 @@ import XCTest // MARK: - MessagesViewControllerTests +@MainActor final class MessagesViewControllerTests: XCTestCase { - // MARK: Internal - var sut: MessagesViewController! - - // swiftlint:enable weak_delegate - - // MARK: - Overridden Methods - - override func setUp() { - super.setUp() - - sut = MessagesViewController() - sut.messagesCollectionView.messagesLayoutDelegate = layoutDelegate - sut.messagesCollectionView.messagesDisplayDelegate = layoutDelegate - _ = sut.view - sut.beginAppearanceTransition(true, animated: true) - sut.endAppearanceTransition() - sut.view.layoutIfNeeded() - } - - override func tearDown() { - sut = nil - - super.tearDown() - } - - // MARK: - Test - - func testNumberOfSectionWithoutData_isZero() { - let messagesDataSource = MockMessagesDataSource() - sut.messagesCollectionView.messagesDataSource = messagesDataSource - - XCTAssertEqual(sut.messagesCollectionView.numberOfSections, 0) - } - - func testNumberOfSection_isNumberOfMessages() { - let messagesDataSource = MockMessagesDataSource() - sut.messagesCollectionView.messagesDataSource = messagesDataSource - messagesDataSource.messages = makeMessages(for: messagesDataSource.senders) - - sut.messagesCollectionView.reloadData() - - let count = sut.messagesCollectionView.numberOfSections - let expectedCount = messagesDataSource.numberOfSections(in: sut.messagesCollectionView) - - XCTAssertEqual(count, expectedCount) - } - - func testNumberOfItemInSection_isOne() { - let messagesDataSource = MockMessagesDataSource() - sut.messagesCollectionView.messagesDataSource = messagesDataSource - messagesDataSource.messages = makeMessages(for: messagesDataSource.senders) - - sut.messagesCollectionView.reloadData() - - XCTAssertEqual(sut.messagesCollectionView.numberOfItems(inSection: 0), 1) - XCTAssertEqual(sut.messagesCollectionView.numberOfItems(inSection: 1), 1) - } - - func testCellForItemWithTextData_returnsTextMessageCell() { - let messagesDataSource = MockMessagesDataSource() - sut.messagesCollectionView.messagesDataSource = messagesDataSource - messagesDataSource.messages.append(MockMessage( - text: "Test", - user: messagesDataSource.senders[0], - messageId: "test_id")) - - sut.messagesCollectionView.reloadData() - - let cell = sut.messagesCollectionView.dataSource?.collectionView( - sut.messagesCollectionView, - cellForItemAt: IndexPath(item: 0, section: 0)) - - XCTAssertNotNil(cell) - XCTAssertTrue(cell is TextMessageCell) - } - - func testCellForItemWithAttributedTextData_returnsTextMessageCell() { - let messagesDataSource = MockMessagesDataSource() - sut.messagesCollectionView.messagesDataSource = messagesDataSource - let attributes = [NSAttributedString.Key.foregroundColor: UIColor.outgoingMessageLabel] - let attriutedString = NSAttributedString(string: "Test", attributes: attributes) - messagesDataSource.messages.append(MockMessage( - attributedText: attriutedString, - user: messagesDataSource.senders[0], - messageId: "test_id")) - - sut.messagesCollectionView.reloadData() - - let cell = sut.messagesCollectionView.dataSource?.collectionView( - sut.messagesCollectionView, - cellForItemAt: IndexPath(item: 0, section: 0)) - - XCTAssertNotNil(cell) - XCTAssertTrue(cell is TextMessageCell) - } - - func testCellForItemWithPhotoData_returnsMediaMessageCell() { - let messagesDataSource = MockMessagesDataSource() - sut.messagesCollectionView.messagesDataSource = messagesDataSource - messagesDataSource.messages.append(MockMessage( - image: UIImage(), - user: messagesDataSource.senders[0], - messageId: "test_id")) - - sut.messagesCollectionView.reloadData() - - let cell = sut.messagesCollectionView.dataSource?.collectionView( - sut.messagesCollectionView, - cellForItemAt: IndexPath(item: 0, section: 0)) - - XCTAssertNotNil(cell) - XCTAssertTrue(cell is MediaMessageCell) - } - - func testCellForItemWithVideoData_returnsMediaMessageCell() { - let messagesDataSource = MockMessagesDataSource() - sut.messagesCollectionView.messagesDataSource = messagesDataSource - messagesDataSource.messages.append(MockMessage( - thumbnail: UIImage(), - user: messagesDataSource.senders[0], - messageId: "test_id")) - - sut.messagesCollectionView.reloadData() - - let cell = sut.messagesCollectionView.dataSource?.collectionView( - sut.messagesCollectionView, - cellForItemAt: IndexPath(item: 0, section: 0)) - - XCTAssertNotNil(cell) - XCTAssertTrue(cell is MediaMessageCell) - } - - func testCellForItemWithLocationData_returnsLocationMessageCell() { - let messagesDataSource = MockMessagesDataSource() - sut.messagesCollectionView.messagesDataSource = messagesDataSource - messagesDataSource.messages.append(MockMessage( - location: CLLocation(latitude: 60.0, longitude: 70.0), - user: messagesDataSource.senders[0], - messageId: "test_id")) - - sut.messagesCollectionView.reloadData() - - let cell = sut.messagesCollectionView.dataSource?.collectionView( - sut.messagesCollectionView, - cellForItemAt: IndexPath(item: 0, section: 0)) - - XCTAssertNotNil(cell) - XCTAssertTrue(cell is LocationMessageCell) - } - - func testCellForItemWithAudioData_returnsAudioMessageCell() { - let messagesDataSource = MockMessagesDataSource() - sut.messagesCollectionView.messagesDataSource = messagesDataSource - messagesDataSource.messages.append(MockMessage( - audioURL: URL(fileURLWithPath: ""), - duration: 4.0, - user: messagesDataSource.senders[0], - messageId: "test_id")) - - sut.messagesCollectionView.reloadData() - - let cell = sut.messagesCollectionView.dataSource?.collectionView( - sut.messagesCollectionView, - cellForItemAt: IndexPath(item: 0, section: 0)) - - XCTAssertNotNil(cell) - XCTAssertTrue(cell is AudioMessageCell) - } - - func testCellForItemWithLinkPreviewData_returnsLinkPreviewMessageCell() { - let messagesDataSource = MockMessagesDataSource() - sut.messagesCollectionView.messagesDataSource = messagesDataSource - - let linkItem = MockLinkItem( - text: "https://link.test", - attributedText: nil, - url: URL(string: "https://github.com/MessageKit")!, - title: "Link Title", - teaser: "Link Teaser", - thumbnailImage: UIImage()) - - messagesDataSource.messages.append(MockMessage( - linkItem: linkItem, - user: messagesDataSource.senders[0], - messageId: "test_id")) - - sut.messagesCollectionView.reloadData() - - let cell = sut.messagesCollectionView.dataSource?.collectionView( - sut.messagesCollectionView, - cellForItemAt: IndexPath(item: 0, section: 0)) - - XCTAssertNotNil(cell) - XCTAssertTrue(cell is LinkPreviewMessageCell) - } - - // MARK: - Setups - - func testSubviewsSetup() { - let controller = MessagesViewController() - XCTAssertTrue(controller.view.subviews.contains(controller.messagesCollectionView)) - } - - func testDelegateAndDataSourceSetup() { - let controller = MessagesViewController() - controller.view.layoutIfNeeded() - XCTAssertTrue(controller.messagesCollectionView.delegate is MessagesViewController) - XCTAssertTrue(controller.messagesCollectionView.dataSource is MessagesViewController) - } - - func testDefaultPropertyValues() { - let controller = MessagesViewController() - XCTAssertNotNil(controller.messagesCollectionView) - XCTAssertTrue(controller.messagesCollectionView.collectionViewLayout is MessagesCollectionViewFlowLayout) - - controller.view.layoutIfNeeded() - XCTAssertTrue(controller.extendedLayoutIncludesOpaqueBars) - XCTAssertEqual(controller.view.backgroundColor, UIColor.collectionViewBackground) - XCTAssertEqual(controller.messagesCollectionView.keyboardDismissMode, UIScrollView.KeyboardDismissMode.interactive) - XCTAssertTrue(controller.messagesCollectionView.alwaysBounceVertical) - } - - // MARK: Private - - // swiftlint:disable weak_delegate - private var layoutDelegate = MockLayoutDelegate() - - // MARK: - Assistants - - private func makeMessages(for senders: [MockUser]) -> [MessageType] { - [ - MockMessage(text: "Text 1", user: senders[0], messageId: "test_id_1"), - MockMessage(text: "Text 2", user: senders[1], messageId: "test_id_2"), - ] - } + // MARK: - Private helper API + + private func makeSUT() -> MessagesViewController { + let sut = MessagesViewController() + sut.messagesCollectionView.messagesLayoutDelegate = layoutDelegate + sut.messagesCollectionView.messagesDisplayDelegate = layoutDelegate + _ = sut.view + sut.beginAppearanceTransition(true, animated: true) + sut.endAppearanceTransition() + sut.view.layoutIfNeeded() + + return sut + } + + // MARK: - Test + + func testNumberOfSectionWithoutData_isZero() { + let messagesDataSource = MockMessagesDataSource() + let sut = makeSUT() + sut.messagesCollectionView.messagesDataSource = messagesDataSource + + XCTAssertEqual(sut.messagesCollectionView.numberOfSections, 0) + } + + func testNumberOfSection_isNumberOfMessages() { + let messagesDataSource = MockMessagesDataSource() + let sut = makeSUT() + sut.messagesCollectionView.messagesDataSource = messagesDataSource + messagesDataSource.messages = makeMessages(for: messagesDataSource.senders) + + sut.messagesCollectionView.reloadData() + + let count = sut.messagesCollectionView.numberOfSections + let expectedCount = messagesDataSource.numberOfSections(in: sut.messagesCollectionView) + + XCTAssertEqual(count, expectedCount) + } + + func testNumberOfItemInSection_isOne() { + let messagesDataSource = MockMessagesDataSource() + let sut = makeSUT() + sut.messagesCollectionView.messagesDataSource = messagesDataSource + messagesDataSource.messages = makeMessages(for: messagesDataSource.senders) + + sut.messagesCollectionView.reloadData() + + XCTAssertEqual(sut.messagesCollectionView.numberOfItems(inSection: 0), 1) + XCTAssertEqual(sut.messagesCollectionView.numberOfItems(inSection: 1), 1) + } + + func testCellForItemWithTextData_returnsTextMessageCell() { + let messagesDataSource = MockMessagesDataSource() + let sut = makeSUT() + sut.messagesCollectionView.messagesDataSource = messagesDataSource + messagesDataSource.messages.append(MockMessage( + text: "Test", + user: messagesDataSource.senders[0], + messageId: "test_id")) + + sut.messagesCollectionView.reloadData() + + let cell = sut.messagesCollectionView.dataSource?.collectionView( + sut.messagesCollectionView, + cellForItemAt: IndexPath(item: 0, section: 0)) + + XCTAssertNotNil(cell) + XCTAssertTrue(cell is TextMessageCell) + } + + func testCellForItemWithAttributedTextData_returnsTextMessageCell() { + let messagesDataSource = MockMessagesDataSource() + let sut = makeSUT() + sut.messagesCollectionView.messagesDataSource = messagesDataSource + let attributes = [NSAttributedString.Key.foregroundColor: UIColor.outgoingMessageLabel] + let attriutedString = NSAttributedString(string: "Test", attributes: attributes) + messagesDataSource.messages.append(MockMessage( + attributedText: attriutedString, + user: messagesDataSource.senders[0], + messageId: "test_id")) + + sut.messagesCollectionView.reloadData() + + let cell = sut.messagesCollectionView.dataSource?.collectionView( + sut.messagesCollectionView, + cellForItemAt: IndexPath(item: 0, section: 0)) + + XCTAssertNotNil(cell) + XCTAssertTrue(cell is TextMessageCell) + } + + func testCellForItemWithPhotoData_returnsMediaMessageCell() { + let messagesDataSource = MockMessagesDataSource() + let sut = makeSUT() + sut.messagesCollectionView.messagesDataSource = messagesDataSource + messagesDataSource.messages.append(MockMessage( + image: UIImage(), + user: messagesDataSource.senders[0], + messageId: "test_id")) + + sut.messagesCollectionView.reloadData() + + let cell = sut.messagesCollectionView.dataSource?.collectionView( + sut.messagesCollectionView, + cellForItemAt: IndexPath(item: 0, section: 0)) + + XCTAssertNotNil(cell) + XCTAssertTrue(cell is MediaMessageCell) + } + + func testCellForItemWithVideoData_returnsMediaMessageCell() { + let messagesDataSource = MockMessagesDataSource() + let sut = makeSUT() + sut.messagesCollectionView.messagesDataSource = messagesDataSource + messagesDataSource.messages.append(MockMessage( + thumbnail: UIImage(), + user: messagesDataSource.senders[0], + messageId: "test_id")) + + sut.messagesCollectionView.reloadData() + + let cell = sut.messagesCollectionView.dataSource?.collectionView( + sut.messagesCollectionView, + cellForItemAt: IndexPath(item: 0, section: 0)) + + XCTAssertNotNil(cell) + XCTAssertTrue(cell is MediaMessageCell) + } + + func testCellForItemWithLocationData_returnsLocationMessageCell() { + let messagesDataSource = MockMessagesDataSource() + let sut = makeSUT() + sut.messagesCollectionView.messagesDataSource = messagesDataSource + messagesDataSource.messages.append(MockMessage( + location: CLLocation(latitude: 60.0, longitude: 70.0), + user: messagesDataSource.senders[0], + messageId: "test_id")) + + sut.messagesCollectionView.reloadData() + + let cell = sut.messagesCollectionView.dataSource?.collectionView( + sut.messagesCollectionView, + cellForItemAt: IndexPath(item: 0, section: 0)) + + XCTAssertNotNil(cell) + XCTAssertTrue(cell is LocationMessageCell) + } + + func testCellForItemWithAudioData_returnsAudioMessageCell() { + let messagesDataSource = MockMessagesDataSource() + let sut = makeSUT() + sut.messagesCollectionView.messagesDataSource = messagesDataSource + messagesDataSource.messages.append(MockMessage( + audioURL: URL(fileURLWithPath: ""), + duration: 4.0, + user: messagesDataSource.senders[0], + messageId: "test_id")) + + sut.messagesCollectionView.reloadData() + + let cell = sut.messagesCollectionView.dataSource?.collectionView( + sut.messagesCollectionView, + cellForItemAt: IndexPath(item: 0, section: 0)) + + XCTAssertNotNil(cell) + XCTAssertTrue(cell is AudioMessageCell) + } + + func testCellForItemWithLinkPreviewData_returnsLinkPreviewMessageCell() { + let messagesDataSource = MockMessagesDataSource() + let sut = makeSUT() + sut.messagesCollectionView.messagesDataSource = messagesDataSource + + let linkItem = MockLinkItem( + text: "https://link.test", + attributedText: nil, + url: URL(string: "https://github.com/MessageKit")!, + title: "Link Title", + teaser: "Link Teaser", + thumbnailImage: UIImage()) + + messagesDataSource.messages.append(MockMessage( + linkItem: linkItem, + user: messagesDataSource.senders[0], + messageId: "test_id")) + + sut.messagesCollectionView.reloadData() + + let cell = sut.messagesCollectionView.dataSource?.collectionView( + sut.messagesCollectionView, + cellForItemAt: IndexPath(item: 0, section: 0)) + + XCTAssertNotNil(cell) + XCTAssertTrue(cell is LinkPreviewMessageCell) + } + + // MARK: - Setups + + func testSubviewsSetup() { + let controller = MessagesViewController() + XCTAssertTrue(controller.view.subviews.contains(controller.messagesCollectionView)) + } + + func testDelegateAndDataSourceSetup() { + let controller = MessagesViewController() + controller.view.layoutIfNeeded() + XCTAssertTrue(controller.messagesCollectionView.delegate is MessagesViewController) + XCTAssertTrue(controller.messagesCollectionView.dataSource is MessagesViewController) + } + + func testDefaultPropertyValues() { + let controller = MessagesViewController() + XCTAssertNotNil(controller.messagesCollectionView) + XCTAssertTrue(controller.messagesCollectionView.collectionViewLayout is MessagesCollectionViewFlowLayout) + + controller.view.layoutIfNeeded() + XCTAssertTrue(controller.extendedLayoutIncludesOpaqueBars) + XCTAssertEqual(controller.view.backgroundColor, UIColor.collectionViewBackground) + XCTAssertEqual(controller.messagesCollectionView.keyboardDismissMode, UIScrollView.KeyboardDismissMode.interactive) + XCTAssertTrue(controller.messagesCollectionView.alwaysBounceVertical) + } + + // MARK: Private + + // swiftlint:disable:next weak_delegate + private var layoutDelegate = MockLayoutDelegate() + + // MARK: - Assistants + + private func makeMessages(for senders: [MockUser]) -> [MessageType] { + [ + MockMessage(text: "Text 1", user: senders[0], messageId: "test_id_1"), + MockMessage(text: "Text 2", user: senders[1], messageId: "test_id_2"), + ] + } } // MARK: - MockLayoutDelegate private class MockLayoutDelegate: MessagesLayoutDelegate, MessagesDisplayDelegate { - // MARK: - LocationMessageLayoutDelegate + // MARK: - LocationMessageLayoutDelegate - func heightForLocation(message _: MessageType, at _: IndexPath, with _: CGFloat, in _: MessagesCollectionView) -> CGFloat { - 0.0 - } + func heightForLocation(message _: MessageType, at _: IndexPath, with _: CGFloat, in _: MessagesCollectionView) -> CGFloat { + 0.0 + } - func heightForMedia(message _: MessageType, at _: IndexPath, with _: CGFloat, in _: MessagesCollectionView) -> CGFloat { - 10.0 - } + func heightForMedia(message _: MessageType, at _: IndexPath, with _: CGFloat, in _: MessagesCollectionView) -> CGFloat { + 10.0 + } - func snapshotOptionsForLocation( - message _: MessageType, - at _: IndexPath, - in _: MessagesCollectionView) + func snapshotOptionsForLocation( + message _: MessageType, + at _: IndexPath, + in _: MessagesCollectionView) -> LocationMessageSnapshotOptions - { - LocationMessageSnapshotOptions() - } + { + LocationMessageSnapshotOptions() + } } diff --git a/Tests/MessageKitTests/MessageKitTests.swift b/Tests/MessageKitTests/MessageKitTests.swift index 325faa977..1b5169b52 100644 --- a/Tests/MessageKitTests/MessageKitTests.swift +++ b/Tests/MessageKitTests/MessageKitTests.swift @@ -25,7 +25,7 @@ import XCTest @testable import MessageKit final class MessageKitTests: XCTestCase { - static var allTests = [ - "", - ] + static let allTests = [ + "", + ] } diff --git a/Tests/MessageKitTests/Mocks/MockMessagesDataSource.swift b/Tests/MessageKitTests/Mocks/MockMessagesDataSource.swift index d89240f64..dc9da0138 100644 --- a/Tests/MessageKitTests/Mocks/MockMessagesDataSource.swift +++ b/Tests/MessageKitTests/Mocks/MockMessagesDataSource.swift @@ -23,26 +23,27 @@ import Foundation @testable import MessageKit +@MainActor class MockMessagesDataSource: MessagesDataSource { - var messages: [MessageType] = [] - let senders: [MockUser] = [ - MockUser(senderId: "sender_1", displayName: "Sender 1"), - MockUser(senderId: "sender_2", displayName: "Sender 2"), - ] + var messages: [MessageType] = [] + let senders: [MockUser] = [ + MockUser(senderId: "sender_1", displayName: "Sender 1"), + MockUser(senderId: "sender_2", displayName: "Sender 2"), + ] - var currentUser: MockUser { - senders[0] - } + var currentUser: MockUser { + senders[0] + } - var currentSender: SenderType { - currentUser - } + var currentSender: SenderType { + currentUser + } - func numberOfSections(in _: MessagesCollectionView) -> Int { - messages.count - } + func numberOfSections(in _: MessagesCollectionView) -> Int { + messages.count + } - func messageForItem(at indexPath: IndexPath, in _: MessagesCollectionView) -> MessageType { - messages[indexPath.section] - } + func messageForItem(at indexPath: IndexPath, in _: MessagesCollectionView) -> MessageType { + messages[indexPath.section] + } } diff --git a/Tests/MessageKitTests/Protocols Tests/MessagesDisplayDelegateTests.swift b/Tests/MessageKitTests/Protocols Tests/MessagesDisplayDelegateTests.swift index 79fb809f7..2f51d61b9 100644 --- a/Tests/MessageKitTests/Protocols Tests/MessagesDisplayDelegateTests.swift +++ b/Tests/MessageKitTests/Protocols Tests/MessagesDisplayDelegateTests.swift @@ -26,224 +26,219 @@ import XCTest // MARK: - MessagesDisplayDelegateTests +@MainActor final class MessagesDisplayDelegateTests: XCTestCase { - // MARK: Internal - - override func setUp() { - super.setUp() - - sut = MockMessagesViewController() - _ = sut.view - sut.beginAppearanceTransition(true, animated: true) - sut.endAppearanceTransition() - sut.viewDidLoad() - sut.view.layoutIfNeeded() - } - - override func tearDown() { - sut = nil - - super.tearDown() - } - - func testBackGroundColorDefaultState() { - XCTAssertEqual( - sut.backgroundColor( - for: sut.dataProvider.messages[0], - at: IndexPath(item: 0, section: 0), - in: sut.messagesCollectionView), - UIColor.outgoingMessageBackground) - XCTAssertNotEqual( - sut.backgroundColor( - for: sut.dataProvider.messages[0], - at: IndexPath(item: 0, section: 0), - in: sut.messagesCollectionView), - UIColor.incomingMessageBackground) - XCTAssertEqual( - sut.backgroundColor( - for: sut.dataProvider.messages[1], - at: IndexPath(item: 1, section: 0), - in: sut.messagesCollectionView), - UIColor.incomingMessageBackground) - XCTAssertNotEqual( - sut.backgroundColor( - for: sut.dataProvider.messages[1], - at: IndexPath(item: 1, section: 0), - in: sut.messagesCollectionView), - UIColor.outgoingMessageBackground) - } - - func testBackgroundColorWithoutDataSource_returnsWhiteForDefault() { - sut.messagesCollectionView.messagesDataSource = nil - let backgroundColor = sut.backgroundColor( - for: sut.dataProvider.messages[0], - at: IndexPath(item: 0, section: 0), - in: sut.messagesCollectionView) - - XCTAssertEqual(backgroundColor, UIColor.white) - } - - func testBackgroundColorForMessageWithEmoji_returnsClearForDefault() { - sut.dataProvider.messages.append(MockMessage( - emoji: "🤔", - user: sut.dataProvider.currentUser, - messageId: "003")) - let backgroundColor = sut.backgroundColor( - for: sut.dataProvider.messages[2], - at: IndexPath(item: 0, section: 0), - in: sut.messagesCollectionView) - - XCTAssertEqual(backgroundColor, .clear) - } - - func testCellTopLabelDefaultState() { - XCTAssertNil(sut.dataProvider.cellTopLabelAttributedText( - for: sut.dataProvider.messages[0], - at: IndexPath(item: 0, section: 0))) - } - - func testMessageBottomLabelDefaultState() { - XCTAssertNil(sut.dataProvider.messageBottomLabelAttributedText( - for: sut.dataProvider.messages[0], - at: IndexPath(item: 0, section: 0))) - } - - func testMessageStyle_returnsBubleTypeForDefault() { - let type = sut.messageStyle( - for: sut.dataProvider.messages[0], - at: IndexPath(item: 0, section: 0), - in: sut.messagesCollectionView) - - let result: Bool - switch type { - case .bubble: - result = true - default: - result = false - } - - XCTAssertTrue(result) - } - - func testMessageHeaderView_isNotNil() { - let indexPath = IndexPath(item: 0, section: 1) - XCTAssert(sut.dataProvider != nil) - let headerView = sut.messageHeaderView(for: indexPath, in: sut.messagesCollectionView) - XCTAssertNotNil(headerView) - } - - // MARK: Private - - private var sut: MockMessagesViewController! -} + // MARK: - Private helper API -// MARK: - TextMessageDisplayDelegateTests + private func makeSUT() -> MockMessagesViewController { + let sut = MockMessagesViewController() + _ = sut.view + sut.beginAppearanceTransition(true, animated: true) + sut.endAppearanceTransition() + sut.viewDidLoad() + sut.view.layoutIfNeeded() -class TextMessageDisplayDelegateTests: XCTestCase { - // MARK: Internal + return sut + } - override func setUp() { - super.setUp() + // MARK: Internal + + func testBackGroundColorDefaultState() { + let sut = makeSUT() + XCTAssertEqual( + sut.backgroundColor( + for: sut.dataProvider.messages[0], + at: IndexPath(item: 0, section: 0), + in: sut.messagesCollectionView), + UIColor.outgoingMessageBackground) + XCTAssertNotEqual( + sut.backgroundColor( + for: sut.dataProvider.messages[0], + at: IndexPath(item: 0, section: 0), + in: sut.messagesCollectionView), + UIColor.incomingMessageBackground) + XCTAssertEqual( + sut.backgroundColor( + for: sut.dataProvider.messages[1], + at: IndexPath(item: 1, section: 0), + in: sut.messagesCollectionView), + UIColor.incomingMessageBackground) + XCTAssertNotEqual( + sut.backgroundColor( + for: sut.dataProvider.messages[1], + at: IndexPath(item: 1, section: 0), + in: sut.messagesCollectionView), + UIColor.outgoingMessageBackground) + } + + func testBackgroundColorWithoutDataSource_returnsWhiteForDefault() { + let sut = makeSUT() + sut.messagesCollectionView.messagesDataSource = nil + let backgroundColor = sut.backgroundColor( + for: sut.dataProvider.messages[0], + at: IndexPath(item: 0, section: 0), + in: sut.messagesCollectionView) - sut = MockMessagesViewController() - _ = sut.view - sut.beginAppearanceTransition(true, animated: true) - sut.endAppearanceTransition() - } + XCTAssertEqual(backgroundColor, UIColor.white) + } - override func tearDown() { - sut = nil + func testBackgroundColorForMessageWithEmoji_returnsClearForDefault() { + let sut = makeSUT() + sut.dataProvider.messages.append(MockMessage( + emoji: "🤔", + user: sut.dataProvider.currentUser, + messageId: "003")) + let backgroundColor = sut.backgroundColor( + for: sut.dataProvider.messages[2], + at: IndexPath(item: 0, section: 0), + in: sut.messagesCollectionView) + + XCTAssertEqual(backgroundColor, .clear) + } - super.tearDown() - } + func testCellTopLabelDefaultState() { + let sut = makeSUT() + XCTAssertNil(sut.dataProvider.cellTopLabelAttributedText( + for: sut.dataProvider.messages[0], + at: IndexPath(item: 0, section: 0))) + } + + func testMessageBottomLabelDefaultState() { + let sut = makeSUT() + XCTAssertNil(sut.dataProvider.messageBottomLabelAttributedText( + for: sut.dataProvider.messages[0], + at: IndexPath(item: 0, section: 0))) + } + + func testMessageStyle_returnsBubleTypeForDefault() { + let sut = makeSUT() + let type = sut.messageStyle( + for: sut.dataProvider.messages[0], + at: IndexPath(item: 0, section: 0), + in: sut.messagesCollectionView) + + let result: Bool + switch type { + case .bubble: + result = true + default: + result = false + } + + XCTAssertTrue(result) + } + + func testMessageHeaderView_isNotNil() { + let sut = makeSUT() + let indexPath = IndexPath(item: 0, section: 1) + XCTAssert(sut.dataProvider != nil) + let headerView = sut.messageHeaderView(for: indexPath, in: sut.messagesCollectionView) + XCTAssertNotNil(headerView) + } +} - func testTextColorFromCurrentSender_returnsWhiteForDefault() { - let textColor = sut.textColor( - for: sut.dataProvider.messages[0], - at: IndexPath(item: 0, section: 0), - in: sut.messagesCollectionView) +// MARK: - TextMessageDisplayDelegateTests - XCTAssertEqual(textColor, UIColor.outgoingMessageLabel) - } +@MainActor +class TextMessageDisplayDelegateTests: XCTestCase { + // MARK: - Private helper API + + private func makeSUT() -> MockMessagesViewController { + let sut = MockMessagesViewController() + _ = sut.view + sut.beginAppearanceTransition(true, animated: true) + sut.endAppearanceTransition() + return sut + } - func testTextColorFromYou_returnsDarkTextForDefault() { - let textColor = sut.textColor( - for: sut.dataProvider.messages[1], - at: IndexPath(item: 0, section: 0), - in: sut.messagesCollectionView) + func testTextColorFromCurrentSender_returnsWhiteForDefault() { + let sut = makeSUT() + let textColor = sut.textColor( + for: sut.dataProvider.messages[0], + at: IndexPath(item: 0, section: 0), + in: sut.messagesCollectionView) - XCTAssertEqual(textColor, UIColor.incomingMessageLabel) - } + XCTAssertEqual(textColor, UIColor.outgoingMessageLabel) + } - func testTextColorWithoutDataSource_returnsDarkTextForDefault() { - let dataSource = sut.makeDataSource() - sut.messagesCollectionView.messagesDataSource = dataSource - let textColor = sut.textColor( - for: sut.dataProvider.messages[1], - at: IndexPath(item: 0, section: 0), - in: sut.messagesCollectionView) + func testTextColorFromYou_returnsDarkTextForDefault() { + let sut = makeSUT() + let textColor = sut.textColor( + for: sut.dataProvider.messages[1], + at: IndexPath(item: 0, section: 0), + in: sut.messagesCollectionView) - XCTAssertEqual(textColor, UIColor.incomingMessageLabel) - } + XCTAssertEqual(textColor, UIColor.incomingMessageLabel) + } - func testEnableDetectors_returnsEmptyForDefault() { - let detectors = sut.enabledDetectors( - for: sut.dataProvider.messages[1], - at: IndexPath(item: 0, section: 0), - in: sut.messagesCollectionView) - let expectedDetectors: [DetectorType] = [] + func testTextColorWithoutDataSource_returnsDarkTextForDefault() { + let sut = makeSUT() + let dataSource = sut.makeDataSource() + sut.messagesCollectionView.messagesDataSource = dataSource + let textColor = sut.textColor( + for: sut.dataProvider.messages[1], + at: IndexPath(item: 0, section: 0), + in: sut.messagesCollectionView) - XCTAssertEqual(detectors, expectedDetectors) - } + XCTAssertEqual(textColor, UIColor.incomingMessageLabel) + } - // MARK: Private + func testEnableDetectors_returnsEmptyForDefault() { + let sut = makeSUT() + let detectors = sut.enabledDetectors( + for: sut.dataProvider.messages[1], + at: IndexPath(item: 0, section: 0), + in: sut.messagesCollectionView) + let expectedDetectors: [DetectorType] = [] - private var sut: MockMessagesViewController! + XCTAssertEqual(detectors, expectedDetectors) + } } // MARK: - MockMessagesViewController +@MainActor private class MockMessagesViewController: MessagesViewController, MessagesDisplayDelegate, MessagesLayoutDelegate { - // MARK: Internal + // MARK: Internal - var dataProvider: MockMessagesDataSource! + var dataProvider: MockMessagesDataSource! - func heightForLocation(message _: MessageType, at _: IndexPath, with _: CGFloat, in _: MessagesCollectionView) -> CGFloat { - 200 - } + func heightForLocation(message _: MessageType, at _: IndexPath, with _: CGFloat, in _: MessagesCollectionView) -> CGFloat { + 200 + } - override func viewDidLoad() { - super.viewDidLoad() + override func viewDidLoad() { + super.viewDidLoad() - dataProvider = makeDataSource() - messagesCollectionView.messagesDisplayDelegate = self - messagesCollectionView.messagesDataSource = dataProvider - messagesCollectionView.messagesLayoutDelegate = self - messagesCollectionView.reloadData() - } + dataProvider = makeDataSource() + messagesCollectionView.messagesDisplayDelegate = self + messagesCollectionView.messagesDataSource = dataProvider + messagesCollectionView.messagesLayoutDelegate = self + messagesCollectionView.reloadData() + } - func snapshotOptionsForLocation( - message _: MessageType, - at _: IndexPath, - in _: MessagesCollectionView) + func snapshotOptionsForLocation( + message _: MessageType, + at _: IndexPath, + in _: MessagesCollectionView) -> LocationMessageSnapshotOptions - { - LocationMessageSnapshotOptions() - } - - // MARK: Fileprivate - - fileprivate func makeDataSource() -> MockMessagesDataSource { - let dataSource = MockMessagesDataSource() - dataSource.messages.append(MockMessage( - text: "Text 1", - user: dataSource.senders[0], - messageId: "001")) - dataSource.messages.append(MockMessage( - text: "Text 2", - user: dataSource.senders[1], - messageId: "002")) - - return dataSource - } + { + LocationMessageSnapshotOptions() + } + + // MARK: Fileprivate + + fileprivate func makeDataSource() -> MockMessagesDataSource { + let dataSource = MockMessagesDataSource() + dataSource.messages.append(MockMessage( + text: "Text 1", + user: dataSource.senders[0], + messageId: "001")) + dataSource.messages.append(MockMessage( + text: "Text 2", + user: dataSource.senders[1], + messageId: "002")) + + return dataSource + } } diff --git a/Tests/MessageKitTests/Views Tests/AvatarViewTests.swift b/Tests/MessageKitTests/Views Tests/AvatarViewTests.swift index ad3f99dd4..846b1c01e 100644 --- a/Tests/MessageKitTests/Views Tests/AvatarViewTests.swift +++ b/Tests/MessageKitTests/Views Tests/AvatarViewTests.swift @@ -24,62 +24,69 @@ import Foundation import XCTest @testable import MessageKit +@MainActor final class AvatarViewTests: XCTestCase { - var avatarView: AvatarView! + // MARK: - Private helper API - override func setUp() { - super.setUp() - avatarView = AvatarView() - avatarView.frame.size = CGSize(width: 30, height: 30) - } + private func makeAvatarView() -> AvatarView { + let avatarView = AvatarView() + avatarView.frame.size = CGSize(width: 30, height: 30) + return avatarView + } - override func tearDown() { - super.tearDown() - avatarView = nil - } + func testNoParams() { + let avatarView = makeAvatarView() - func testNoParams() { - XCTAssertEqual(avatarView.layer.cornerRadius, 15.0) - // For certain dynamic colors, need to compare cgColor in XCTest - // https://stackoverflow.com/questions/58065340/how-to-compare-two-uidynamicprovidercolor - XCTAssertEqual(avatarView.backgroundColor!.cgColor, UIColor.avatarViewBackground.cgColor) - } + XCTAssertEqual(avatarView.layer.cornerRadius, 15.0) + // For certain dynamic colors, need to compare cgColor in XCTest + // https://stackoverflow.com/questions/58065340/how-to-compare-two-uidynamicprovidercolor + XCTAssertEqual(avatarView.backgroundColor!.cgColor, UIColor.avatarViewBackground.cgColor) + } - func testWithImage() { - let avatar = Avatar(image: UIImage()) - avatarView.set(avatar: avatar) - XCTAssertEqual(avatar.initials, "?") - XCTAssertEqual(avatarView.layer.cornerRadius, 15.0) - XCTAssertEqual(avatarView.backgroundColor!.cgColor, UIColor.avatarViewBackground.cgColor) - } + func testWithImage() { + let avatarView = makeAvatarView() - func testInitialsOnly() { - let avatar = Avatar(initials: "DL") - avatarView.set(avatar: avatar) - XCTAssertEqual(avatarView.initials, avatar.initials) - XCTAssertEqual(avatar.initials, "DL") - XCTAssertEqual(avatarView.layer.cornerRadius, 15.0) - XCTAssertEqual(avatarView.backgroundColor!.cgColor, UIColor.avatarViewBackground.cgColor) - } + let avatar = Avatar(image: UIImage()) + avatarView.set(avatar: avatar) + XCTAssertEqual(avatar.initials, "?") + XCTAssertEqual(avatarView.layer.cornerRadius, 15.0) + XCTAssertEqual(avatarView.backgroundColor!.cgColor, UIColor.avatarViewBackground.cgColor) + } - func testSetBackground() { - XCTAssertEqual(avatarView.backgroundColor!.cgColor, UIColor.avatarViewBackground.cgColor) - avatarView.backgroundColor = UIColor.red - XCTAssertEqual(avatarView.backgroundColor!, UIColor.red) - } + func testInitialsOnly() { + let avatarView = makeAvatarView() - func testGetImage() { - let image = UIImage() - let avatar = Avatar(image: image) - avatarView.set(avatar: avatar) - XCTAssertEqual(avatarView.image, image) - } + let avatar = Avatar(initials: "DL") + avatarView.set(avatar: avatar) + XCTAssertEqual(avatarView.initials, avatar.initials) + XCTAssertEqual(avatar.initials, "DL") + XCTAssertEqual(avatarView.layer.cornerRadius, 15.0) + XCTAssertEqual(avatarView.backgroundColor!.cgColor, UIColor.avatarViewBackground.cgColor) + } - func testRoundedCorners() { - let avatar = Avatar(image: UIImage()) - avatarView.set(avatar: avatar) - XCTAssertEqual(avatarView.layer.cornerRadius, 15.0) - avatarView.setCorner(radius: 2) - XCTAssertEqual(avatarView.layer.cornerRadius, 2.0) - } + func testSetBackground() { + let avatarView = makeAvatarView() + XCTAssertEqual(avatarView.backgroundColor!.cgColor, UIColor.avatarViewBackground.cgColor) + avatarView.backgroundColor = UIColor.red + XCTAssertEqual(avatarView.backgroundColor!, UIColor.red) + } + + func testGetImage() { + let avatarView = makeAvatarView() + + let image = UIImage() + let avatar = Avatar(image: image) + avatarView.set(avatar: avatar) + XCTAssertEqual(avatarView.image, image) + } + + func testRoundedCorners() { + let avatarView = makeAvatarView() + + let avatar = Avatar(image: UIImage()) + avatarView.set(avatar: avatar) + XCTAssertEqual(avatarView.layer.cornerRadius, 15.0) + avatarView.setCorner(radius: 2) + XCTAssertEqual(avatarView.layer.cornerRadius, 2.0) + } } diff --git a/Tests/MessageKitTests/Views Tests/MessageCollectionViewCellTests.swift b/Tests/MessageKitTests/Views Tests/MessageCollectionViewCellTests.swift index 382f5d9d6..62190e3ea 100644 --- a/Tests/MessageKitTests/Views Tests/MessageCollectionViewCellTests.swift +++ b/Tests/MessageKitTests/Views Tests/MessageCollectionViewCellTests.swift @@ -26,62 +26,56 @@ import XCTest // MARK: - MessageContentCellTests +@MainActor final class MessageContentCellTests: XCTestCase { - var cell: MessageContentCell! - let frame = CGRect(x: 0, y: 0, width: 100, height: 100) + let frame = CGRect(x: 0, y: 0, width: 100, height: 100) - override func setUp() { - super.setUp() - cell = MessageContentCell(frame: frame) - } - - override func tearDown() { - cell = nil - super.tearDown() - } - - func testInit() { - XCTAssertEqual(cell.contentView.autoresizingMask, [.flexibleWidth, .flexibleHeight]) - XCTAssert(cell.contentView.subviews.contains(cell.cellTopLabel)) - XCTAssert(cell.contentView.subviews.contains(cell.messageBottomLabel)) - XCTAssert(cell.contentView.subviews.contains(cell.avatarView)) - XCTAssert(cell.contentView.subviews.contains(cell.messageContainerView)) - } + func testInit() { + let cell = MessageContentCell(frame: frame) + XCTAssertEqual(cell.contentView.autoresizingMask, [.flexibleWidth, .flexibleHeight]) + XCTAssert(cell.contentView.subviews.contains(cell.cellTopLabel)) + XCTAssert(cell.contentView.subviews.contains(cell.messageBottomLabel)) + XCTAssert(cell.contentView.subviews.contains(cell.avatarView)) + XCTAssert(cell.contentView.subviews.contains(cell.messageContainerView)) + } - func testMessageContainerViewPropertiesSetup() { - XCTAssertTrue(cell.messageContainerView.clipsToBounds) - XCTAssertTrue(cell.messageContainerView.layer.masksToBounds) - } + func testMessageContainerViewPropertiesSetup() { + let cell = MessageContentCell(frame: frame) + XCTAssertTrue(cell.messageContainerView.clipsToBounds) + XCTAssertTrue(cell.messageContainerView.layer.masksToBounds) + } - func testPrepareForReuse() { - cell.prepareForReuse() - XCTAssertNil(cell.cellTopLabel.text) - XCTAssertNil(cell.cellTopLabel.attributedText) - XCTAssertNil(cell.messageBottomLabel.text) - XCTAssertNil(cell.messageBottomLabel.attributedText) - } + func testPrepareForReuse() { + let cell = MessageContentCell(frame: frame) + cell.prepareForReuse() + XCTAssertNil(cell.cellTopLabel.text) + XCTAssertNil(cell.cellTopLabel.attributedText) + XCTAssertNil(cell.messageBottomLabel.text) + XCTAssertNil(cell.messageBottomLabel.attributedText) + } - func testApplyLayoutAttributes() { - let layoutAttributes = MessagesCollectionViewLayoutAttributes() - layoutAttributes.avatarPosition = AvatarPosition(horizontal: .cellLeading, vertical: .cellBottom) - cell.apply(layoutAttributes) + func testApplyLayoutAttributes() { + let cell = MessageContentCell(frame: frame) + let layoutAttributes = MessagesCollectionViewLayoutAttributes() + layoutAttributes.avatarPosition = AvatarPosition(horizontal: .cellLeading, vertical: .cellBottom) + cell.apply(layoutAttributes) - XCTAssertEqual(cell.avatarView.frame, layoutAttributes.frame) - XCTAssertEqual(cell.messageContainerView.frame.size, layoutAttributes.messageContainerSize) - XCTAssertEqual(cell.cellTopLabel.frame.size, layoutAttributes.cellTopLabelSize) - XCTAssertEqual(cell.messageBottomLabel.frame.size, layoutAttributes.messageBottomLabelSize) - } + XCTAssertEqual(cell.avatarView.frame, layoutAttributes.frame) + XCTAssertEqual(cell.messageContainerView.frame.size, layoutAttributes.messageContainerSize) + XCTAssertEqual(cell.cellTopLabel.frame.size, layoutAttributes.cellTopLabelSize) + XCTAssertEqual(cell.messageBottomLabel.frame.size, layoutAttributes.messageBottomLabelSize) + } } extension MessageContentCellTests { - private class MockMessagesDisplayDelegate: MessagesDisplayDelegate { - func snapshotOptionsForLocation( - message _: MessageType, - at _: IndexPath, - in _: MessagesCollectionView) - -> LocationMessageSnapshotOptions - { - LocationMessageSnapshotOptions() + private class MockMessagesDisplayDelegate: MessagesDisplayDelegate { + func snapshotOptionsForLocation( + message _: MessageType, + at _: IndexPath, + in _: MessagesCollectionView) + -> LocationMessageSnapshotOptions + { + LocationMessageSnapshotOptions() + } } - } } diff --git a/Tests/MessageKitTests/Views Tests/MessagesCollectionViewTests.swift b/Tests/MessageKitTests/Views Tests/MessagesCollectionViewTests.swift index 0da2a55f3..84e0ca902 100644 --- a/Tests/MessageKitTests/Views Tests/MessagesCollectionViewTests.swift +++ b/Tests/MessageKitTests/Views Tests/MessagesCollectionViewTests.swift @@ -23,24 +23,15 @@ import XCTest @testable import MessageKit +@MainActor class MessagesCollectionViewTests: XCTestCase { - var messagesCollectionView: MessagesCollectionView! - let rect = CGRect(x: 0, y: 0, width: 100, height: 100) - let layout = MessagesCollectionViewFlowLayout() + let rect = CGRect(x: 0, y: 0, width: 100, height: 100) + let layout = MessagesCollectionViewFlowLayout() - override func setUp() { - super.setUp() - messagesCollectionView = MessagesCollectionView(frame: rect, collectionViewLayout: layout) - } - - override func tearDown() { - messagesCollectionView = nil - super.tearDown() - } - - func testInit() { - XCTAssertEqual(messagesCollectionView.frame, rect) - XCTAssertEqual(messagesCollectionView.collectionViewLayout, layout) - XCTAssertEqual(messagesCollectionView.backgroundColor, UIColor.collectionViewBackground) - } + func testInit() { + let messagesCollectionView = MessagesCollectionView(frame: rect, collectionViewLayout: layout) + XCTAssertEqual(messagesCollectionView.frame, rect) + XCTAssertEqual(messagesCollectionView.collectionViewLayout, layout) + XCTAssertEqual(messagesCollectionView.backgroundColor, UIColor.collectionViewBackground) + } }