From ef58abf63be4d2d929ae33eac72452424cdff5d7 Mon Sep 17 00:00:00 2001 From: Enes Karaosman Date: Tue, 9 Apr 2024 14:54:33 +0300 Subject: [PATCH] refactor: replace VideoPlayer dependency with UIKit+SwiftUI combination (#57) * Update Package.resolved * Update MockMessages.swift * feat: activate picture in picture background mode * refactor!: drop iOS 14 and macOS 11 support * refactor: remove VideoPlayer dependency * chore: update file syntax * fix: correct several issues * Update README.md * Update swift.yml * chore: add changelog * Update MockMessages.swift --- .github/workflows/swift.yml | 10 +- CHANGELOG.md | 8 + Package.swift | 8 +- README.md | 6 +- Sources/SwiftyChat/ChatView.swift | 2 +- .../Common/{View => Video}/PIPVideoCell.swift | 36 ++-- .../Common/Video/PlayerViewModel.swift | 80 ++++++++ .../SwiftUIViews/CustomControlsView.swift | 92 +++++++++ .../Video/SwiftUIViews/CustomPlayerView.swift | 37 ++++ .../SwiftUIViews/CustomVideoPlayer.swift | 58 ++++++ .../Common/Video/UIKitViews/PlayerView.swift | 20 ++ .../Common/View/VideoPlayerContainer.swift | 189 ------------------ .../MessageCells/VideoPlaceholderCell.swift | 4 +- Sources/SwiftyChat/Mock/MockMessages.swift | 30 +-- Sources/SwiftyChat/VideoManager.swift | 14 +- .../project.pbxproj | 15 +- .../xcshareddata/swiftpm/Package.resolved | 78 +++----- .../SwiftyChatExample/Info.plist | 14 +- 18 files changed, 394 insertions(+), 307 deletions(-) rename Sources/SwiftyChat/Common/{View => Video}/PIPVideoCell.swift (95%) create mode 100644 Sources/SwiftyChat/Common/Video/PlayerViewModel.swift create mode 100644 Sources/SwiftyChat/Common/Video/SwiftUIViews/CustomControlsView.swift create mode 100644 Sources/SwiftyChat/Common/Video/SwiftUIViews/CustomPlayerView.swift create mode 100644 Sources/SwiftyChat/Common/Video/SwiftUIViews/CustomVideoPlayer.swift create mode 100644 Sources/SwiftyChat/Common/Video/UIKitViews/PlayerView.swift delete mode 100644 Sources/SwiftyChat/Common/View/VideoPlayerContainer.swift diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 125788b2..ab4fb633 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -9,11 +9,19 @@ on: jobs: build: - runs-on: macos-latest + runs-on: macos-14 steps: - uses: actions/checkout@v4 + + - name: List Xcode installations + run: sudo ls -1 /Applications | grep "Xcode" + + - name: Select Xcode 15.2 + run: sudo xcode-select -s /Applications/Xcode_15.2.app/Contents/Developer + - name: Build run: swift build -v + - name: Run tests run: swift test -v diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c6c5a6e..91c7f39a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # CHANGELOG +## [2.5.0](https://github.com/EnesKaraosman/SwiftyChat/releases/tag/2.5.0) +Released on 2024-04-09. + +- refactor: drop iOS-14 and macOS-11 support +- refactor: remove VideoPlayer dependency +- refactor: increase swift tools version to 5.8 +- fix(ci): correct swift build github action + ## [2.4.1](https://github.com/EnesKaraosman/SwiftyChat/releases/tag/2.4.1) Released on 2023-12-10. diff --git a/Package.swift b/Package.swift index dafe2aec..2920d6fd 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.3 +// swift-tools-version:5.8 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -6,8 +6,8 @@ import PackageDescription let package = Package( name: "SwiftyChat", platforms: [ - .iOS(.v14), - .macOS(.v11) + .iOS(.v15), + .macOS(.v12) ], products: [ .library( @@ -18,7 +18,6 @@ let package = Package( // Image downloading library .package(url: "https://github.com/onevcat/Kingfisher.git", from: "7.11.0"), .package(url: "https://github.com/EnesKaraosman/SwiftUIEKtensions.git", from: "0.2.0"), - .package(url: "https://github.com/wxxsw/VideoPlayer.git", from: "1.2.4"), .package(url: "https://github.com/dkk/WrappingHStack.git", from: "2.2.11") ], targets: [ @@ -29,7 +28,6 @@ let package = Package( dependencies: [ .byName(name: "Kingfisher"), .byName(name: "SwiftUIEKtensions"), - .byName(name: "VideoPlayer"), .byName(name: "WrappingHStack") ], diff --git a/README.md b/README.md index 3cc9cf62..a5999535 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -![Version](https://img.shields.io/badge/version-2.1.0-blue) -![Swift 5.3](https://img.shields.io/badge/Swift-5.3-orange.svg) +![Swift 5.8](https://img.shields.io/badge/Swift-5.8-orange.svg) # SwiftyChat @@ -17,7 +16,8 @@ For Flutter version check [this link](https://github.com/EnesKaraosman/swifty_ch ### About Simple Chat Interface to quick start with [built-in](#message-kinds) message cells.
-Fully written in pure SwiftUI. + +> Note: Enable "Picture in Picture" background mode from Xcode "Sign in and Capabilities" to be used in video message kinds (Optional) ### Features - [x] HTML String support like `
  • , ` (not like h1 or font based tag) diff --git a/Sources/SwiftyChat/ChatView.swift b/Sources/SwiftyChat/ChatView.swift index a5cf24cb..a0f16edb 100644 --- a/Sources/SwiftyChat/ChatView.swift +++ b/Sources/SwiftyChat/ChatView.swift @@ -49,7 +49,7 @@ public struct ChatView: View { } .frame(height: messageEditorHeight) .padding(.bottom, 12) - + PIPVideoCell() } .iOS { $0.keyboardAwarePadding() } diff --git a/Sources/SwiftyChat/Common/View/PIPVideoCell.swift b/Sources/SwiftyChat/Common/Video/PIPVideoCell.swift similarity index 95% rename from Sources/SwiftyChat/Common/View/PIPVideoCell.swift rename to Sources/SwiftyChat/Common/Video/PIPVideoCell.swift index ba3d5533..36fe9b3b 100644 --- a/Sources/SwiftyChat/Common/View/PIPVideoCell.swift +++ b/Sources/SwiftyChat/Common/Video/PIPVideoCell.swift @@ -1,6 +1,6 @@ // // PIPVideoCell.swift -// +// // // Created by Enes Karaosman on 9.11.2020. // @@ -15,17 +15,17 @@ internal extension CGSize { } internal struct PIPVideoCell: View { - + @EnvironmentObject var videoManager: VideoManager @EnvironmentObject var model: DeviceOrientationInfo - + @State private var cancellables: Set = .init() @State private var location: CGPoint = .zero @GestureState private var startLocation: CGPoint? = nil - + private let horizontalPadding: CGFloat = 16 - private let aspectRatio: CGFloat = 1.78 - + private let aspectRatio: CGFloat = 1.4 + private func videoFrameHeight(in size: CGSize) -> CGFloat { if videoManager.isFullScreen && model.orientation == .landscape { return size.height @@ -33,7 +33,7 @@ internal struct PIPVideoCell: View { return videoFrameWidth(in: size) / aspectRatio } } - + private func videoFrameWidth(in size: CGSize) -> CGFloat { if videoManager.isFullScreen { return size.width @@ -42,11 +42,11 @@ internal struct PIPVideoCell: View { (size.width / aspectRatio) : abs(size.width - horizontalPadding) // Padding } } - + private enum Corner { case leftTop, leftBottom, rightTop, rightBottom, center } - + /// When we set .position(), sets its center to given point private func rePositionVideoFrame(toCorner: Corner, in size: CGSize) { let inputViewOffset: CGFloat = videoManager.isFullScreen ? 0 : 60 @@ -81,16 +81,16 @@ internal struct PIPVideoCell: View { } } } - + public var body: some View { ZStack { - + if videoManager.isFullScreen { Color.primary.colorInvert() .animation(.linear) .edgesIgnoringSafeArea(.all) } - + GeometryReader { geometry in video .frame(width: videoFrameWidth(in: geometry.size), height: videoFrameHeight(in: geometry.size)) @@ -101,7 +101,7 @@ internal struct PIPVideoCell: View { .animation(.linear(duration: 0.1)) .onAppear { rePositionVideoFrame(toCorner: .rightTop, in: geometry.size) } .onAppear { - + videoManager.$isFullScreen .removeDuplicates() .sink { fullScreen in @@ -110,7 +110,7 @@ internal struct PIPVideoCell: View { } } .store(in: &cancellables) - + model.$orientation .removeDuplicates() .sink(receiveValue: { _ in @@ -127,13 +127,13 @@ internal struct PIPVideoCell: View { } } } - + @ViewBuilder private var video: some View { if let message = videoManager.message, let videoItem = videoManager.videoItem { - VideoPlayerContainer(media: videoItem, message: message) + CustomPlayerView(media: videoItem, message: message) } } - + // MARK: - Drag Gesture private func simpleDrag(in size: CGSize) -> some Gesture { DragGesture() @@ -161,5 +161,5 @@ internal struct PIPVideoCell: View { } } } - } + } } diff --git a/Sources/SwiftyChat/Common/Video/PlayerViewModel.swift b/Sources/SwiftyChat/Common/Video/PlayerViewModel.swift new file mode 100644 index 00000000..b867ce28 --- /dev/null +++ b/Sources/SwiftyChat/Common/Video/PlayerViewModel.swift @@ -0,0 +1,80 @@ +import AVFoundation +import Combine + +final class PlayerViewModel: ObservableObject { + let player = AVPlayer() + @Published var isInPipMode: Bool = false + @Published var isPlaying = false + + @Published var isEditingCurrentTime = false + @Published var currentTime: Double = .zero + @Published var duration: Double? + + private var subscriptions: Set = [] + private var timeObserver: Any? + + deinit { + if let timeObserver = timeObserver { + player.removeTimeObserver(timeObserver) + } + } + + init() { + $isEditingCurrentTime + .dropFirst() + .filter({ $0 == false }) + .sink(receiveValue: { [weak self] _ in + guard let self else { return } + + self.player.seek( + to: CMTime(seconds: self.currentTime, preferredTimescale: 1), + toleranceBefore: .zero, + toleranceAfter: .zero + ) + + if self.player.rate != 0 { + self.player.play() + } + }) + .store(in: &subscriptions) + + player.publisher(for: \.timeControlStatus) + .sink { [weak self] status in + switch status { + case .playing: + self?.isPlaying = true + case .paused: + self?.isPlaying = false + case .waitingToPlayAtSpecifiedRate: + break + @unknown default: + break + } + } + .store(in: &subscriptions) + + timeObserver = player.addPeriodicTimeObserver( + forInterval: CMTime(seconds: 1, preferredTimescale: 600), + queue: .main + ) { [weak self] time in + guard let self else { return } + + if self.isEditingCurrentTime == false { + self.currentTime = time.seconds + } + } + } + + func setCurrentItem(_ item: AVPlayerItem) { + currentTime = .zero + duration = nil + player.replaceCurrentItem(with: item) + + item.publisher(for: \.status) + .filter({ $0 == .readyToPlay }) + .sink(receiveValue: { [weak self] _ in + self?.duration = item.asset.duration.seconds + }) + .store(in: &subscriptions) + } +} diff --git a/Sources/SwiftyChat/Common/Video/SwiftUIViews/CustomControlsView.swift b/Sources/SwiftyChat/Common/Video/SwiftUIViews/CustomControlsView.swift new file mode 100644 index 00000000..8124ee02 --- /dev/null +++ b/Sources/SwiftyChat/Common/Video/SwiftUIViews/CustomControlsView.swift @@ -0,0 +1,92 @@ +import SwiftUI + +struct CustomControlsView: View { + @ObservedObject var playerVM: PlayerViewModel + @EnvironmentObject var videoManager: VideoManager + + init(for playerViewModel: PlayerViewModel) { + self.playerVM = playerViewModel + } + + var body: some View { + HStack { + playPauseButton + durationSlider + fullScreenButton + closeButton + } + .imageScale(.large) + .padding() + .background(.thinMaterial) + } + + private var playPauseButton: some View { + Color.secondary.colorInvert() + .cornerRadius(10) + .frame(width: 50, height: 40) + .overlay( + Image(systemName: playerVM.isPlaying ? "pause.fill" : "play.fill") + .font(Font.body.weight(.semibold)) + .foregroundColor(Color.white) + .padding() + ) + .onTapGesture { + if playerVM.isPlaying { + playerVM.player.pause() + } else { + playerVM.player.play() + } + } + } + + @ViewBuilder + private var durationSlider: some View { + if let duration = playerVM.duration { + Slider( + value: $playerVM.currentTime, + in: 0...duration, + onEditingChanged: { isEditing in + playerVM.isEditingCurrentTime = isEditing + } + ) + } else { + Spacer() + } + } + + private var fullScreenButton: some View { + Color.secondary.colorInvert() + .cornerRadius(10) + .frame(width: 50, height: 40) + .overlay( + Image( + systemName: videoManager.isFullScreen ? + "arrow.down.right.and.arrow.up.left" : + "arrow.up.left.and.arrow.down.right" + ) + .font(Font.body.weight(.semibold)) + .foregroundColor(Color.white) + .padding() + ) + .onTapGesture { + withAnimation { + videoManager.isFullScreen.toggle() + } + } + } + + private var closeButton: some View { + Color.secondary.colorInvert() + .cornerRadius(10) + .frame(width: 50, height: 40) + .overlay( + Image(systemName: "xmark") + .font(Font.body.weight(.semibold)) + .foregroundColor(Color.white) + .padding() + ) + .onTapGesture { + self.videoManager.flushState() + } + } +} diff --git a/Sources/SwiftyChat/Common/Video/SwiftUIViews/CustomPlayerView.swift b/Sources/SwiftyChat/Common/Video/SwiftUIViews/CustomPlayerView.swift new file mode 100644 index 00000000..4cb356ef --- /dev/null +++ b/Sources/SwiftyChat/Common/Video/SwiftUIViews/CustomPlayerView.swift @@ -0,0 +1,37 @@ +import AVFoundation +import SwiftUI + +struct CustomPlayerView: View { + @StateObject private var playerVM = PlayerViewModel() + public let media: VideoItem + public let message: Message + + @EnvironmentObject var style: ChatMessageCellStyle + @EnvironmentObject var videoManager: VideoManager + + private var cellStyle: VideoPlaceholderCellStyle { + style.videoPlaceholderCellStyle + } + + init(media: VideoItem, message: Message) { + self.media = media + self.message = message + + try? AVAudioSession.sharedInstance().setCategory(.playback) + } + + var body: some View { + VStack(spacing: .zero) { + CustomVideoPlayer(playerVM: playerVM) + CustomControlsView(for: playerVM) + } + .clipShape(RoundedRectangle(cornerRadius: cellStyle.cellCornerRadius)) + .onAppear { + playerVM.setCurrentItem(AVPlayerItem(url: media.url)) + playerVM.player.play() + } + .onDisappear { + playerVM.player.pause() + } + } +} diff --git a/Sources/SwiftyChat/Common/Video/SwiftUIViews/CustomVideoPlayer.swift b/Sources/SwiftyChat/Common/Video/SwiftUIViews/CustomVideoPlayer.swift new file mode 100644 index 00000000..19a8d9fe --- /dev/null +++ b/Sources/SwiftyChat/Common/Video/SwiftUIViews/CustomVideoPlayer.swift @@ -0,0 +1,58 @@ +import AVKit +import Combine +import SwiftUI + +struct CustomVideoPlayer: UIViewRepresentable { + @ObservedObject var playerVM: PlayerViewModel + + func makeUIView(context: Context) -> PlayerView { + let view = PlayerView() + view.player = playerVM.player + context.coordinator.setController(view.playerLayer) + return view + } + + func updateUIView(_ uiView: PlayerView, context: Context) { } + + func makeCoordinator() -> Coordinator { + return Coordinator(self) + } + + class Coordinator: NSObject, AVPictureInPictureControllerDelegate { + private let parent: CustomVideoPlayer + private var controller: AVPictureInPictureController? + private var cancellable: AnyCancellable? + + init(_ parent: CustomVideoPlayer) { + self.parent = parent + super.init() + + cancellable = parent.playerVM.$isInPipMode + .sink { [weak self] in + guard let self = self, + let controller = self.controller else { return } + if $0 { + if controller.isPictureInPictureActive == false { + controller.startPictureInPicture() + } + } else if controller.isPictureInPictureActive { + controller.stopPictureInPicture() + } + } + } + + func setController(_ playerLayer: AVPlayerLayer) { + controller = AVPictureInPictureController(playerLayer: playerLayer) + controller?.canStartPictureInPictureAutomaticallyFromInline = true + controller?.delegate = self + } + + func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { + parent.playerVM.isInPipMode = true + } + + func pictureInPictureControllerWillStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { + parent.playerVM.isInPipMode = false + } + } +} diff --git a/Sources/SwiftyChat/Common/Video/UIKitViews/PlayerView.swift b/Sources/SwiftyChat/Common/Video/UIKitViews/PlayerView.swift new file mode 100644 index 00000000..6655907b --- /dev/null +++ b/Sources/SwiftyChat/Common/Video/UIKitViews/PlayerView.swift @@ -0,0 +1,20 @@ +import AVFoundation +import UIKit + +final class PlayerView: UIView { + override static var layerClass: AnyClass { + AVPlayerLayer.self + } + + var playerLayer: AVPlayerLayer { layer as! AVPlayerLayer } + + var player: AVPlayer? { + get { + playerLayer.player + } + set { + playerLayer.videoGravity = .resizeAspectFill + playerLayer.player = newValue + } + } +} diff --git a/Sources/SwiftyChat/Common/View/VideoPlayerContainer.swift b/Sources/SwiftyChat/Common/View/VideoPlayerContainer.swift deleted file mode 100644 index 1f4443c2..00000000 --- a/Sources/SwiftyChat/Common/View/VideoPlayerContainer.swift +++ /dev/null @@ -1,189 +0,0 @@ -// -// VideoPlayerContainer.swift -// -// -// Created by Enes Karaosman on 9.11.2020. -// - -import SwiftUI -import AVFoundation -import VideoPlayer -import SwiftUIEKtensions - -internal struct VideoPlayerContainer: View { - - public let media: VideoItem - public let message: Message - - @EnvironmentObject var videoManager: VideoManager - - @State private var time: CMTime = .zero - @State private var play: Bool = true - @State private var autoReplay: Bool = false - @State private var mute: Bool = false - @State private var totalDuration: Double = 0 - @State private var waitingOverlayToHide: Bool = false - - @State private var showOverlay: Bool = false { - didSet { - if showOverlay && !waitingOverlayToHide { - waitingOverlayToHide = true - DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { - withAnimation { - showOverlay = false - waitingOverlayToHide = false - } - } - } - } - } - - public var body: some View { - ZStack(alignment: .bottom) { - videoPlayer - .onTapGesture { - withAnimation { - showOverlay.toggle() - } - } - .onDisappear { self.play = false } - - videoOverlay - } - } - - // MARK: - VideoPlayer - private var videoPlayer: some View { - VideoPlayer.init(url: media.url, play: $play, time: $time) - .autoReplay(autoReplay) - .mute(mute) - .onBufferChanged { progress in print("onBufferChanged \(progress)") } - .onPlayToEndTime { print("onPlayToEndTime") } - .onReplay { print("onReplay") } - .onStateChanged { state in - switch state { - case .loading: - print("Loading...") - case .playing(let totalDuration): - print("Playing!") - self.totalDuration = totalDuration - case .paused(let playProgress, let bufferProgress): - print("Paused: play \(Int(playProgress * 100))% buffer \(Int(bufferProgress * 100))%") - case .error(let error): - print("Error: \(error)") - } - } - } - - // MARK: - Overlay - private var videoOverlay: some View { - VStack(spacing: 0) { - HStack { - closeButton - Spacer() - fullScreenButton - } - Spacer() - VStack(spacing: 1) { - durationSliderView - - HStack { - Text(self.time.seconds.formatSecondsToHMS()) - .fontWeight(.semibold) - Spacer() - Text(self.totalDuration.formatSecondsToHMS()) - .fontWeight(.semibold) - } - .padding(.horizontal) - .font(.footnote) - - HStack { - - Image(systemName: "gobackward.10") - .resizable() - .scaledToFit() - .frame(width: 30, height: 30) - .onTapGesture { - self.time = CMTimeMakeWithSeconds(max(0, self.time.seconds - 10), preferredTimescale: self.time.timescale) - } - .frame(minWidth: 0, maxWidth: .infinity) - - Image(systemName: self.play ? "pause.circle.fill" : "play.circle.fill") - .resizable() - .scaledToFit() - .frame(width: 30, height: 30) - .onTapGesture { self.play.toggle() } - .frame(minWidth: 0, maxWidth: .infinity) - - Image(systemName: "goforward.10") - .resizable() - .scaledToFit() - .frame(width: 30, height: 30) - .onTapGesture { - self.time = CMTimeMakeWithSeconds(min(self.totalDuration, self.time.seconds + 10), preferredTimescale: self.time.timescale) - } - .frame(minWidth: 0, maxWidth: .infinity) - - } - .padding(.vertical, 4) - } - .background(Color.secondary.colorInvert().blur(radius: 2)) - - } - .hidden(!showOverlay) - } - - // MARK: - VideoOverlayComponents - private var closeButton: some View { - Color.secondary.colorInvert() - .cornerRadius(10) - .frame(width: 50, height: 40) - .overlay( - Image(systemName: "xmark") - .font(Font.body.weight(.semibold)) - .padding() - .foregroundColor(Color.white) - ) - .onTapGesture { - self.videoManager.flushState() - } - } - - private var fullScreenButton: some View { - Color.secondary.colorInvert() - .cornerRadius(10) - .frame(width: 50, height: 40) - .overlay( - Image( - systemName: videoManager.isFullScreen ? - "arrow.down.right.and.arrow.up.left" : - "arrow.up.left.and.arrow.down.right" - ) - .font(Font.body.weight(.semibold)) - .padding() - .foregroundColor(Color.white) - ) - .onTapGesture { - withAnimation { - videoManager.isFullScreen.toggle() - } - } - } - - private var durationSliderView: some View { - Slider( - value: Binding( - get: { time.seconds }, - set: { - self.time = CMTimeMakeWithSeconds( - $0, preferredTimescale: self.time.timescale - ) - } - ), - in: 0...totalDuration - ) - .padding(.horizontal) - .accentColor(.red) - .gesture(DragGesture()) // << To avoid outer dragGesture, slider & position both was changing - } -} diff --git a/Sources/SwiftyChat/MessageCells/VideoPlaceholderCell.swift b/Sources/SwiftyChat/MessageCells/VideoPlaceholderCell.swift index 57433f52..f57b92d2 100644 --- a/Sources/SwiftyChat/MessageCells/VideoPlaceholderCell.swift +++ b/Sources/SwiftyChat/MessageCells/VideoPlaceholderCell.swift @@ -29,7 +29,7 @@ internal struct VideoPlaceholderCell: View { private var imageWidth: CGFloat { cellStyle.cellWidth(size) } - + public var body: some View { thumbnailView .overlay(thumbnailOverlay) @@ -90,7 +90,7 @@ internal struct VideoPlaceholderCell: View { } .foregroundColor(.white) } - + @ViewBuilder private var thumbnailOverlay: some View { if isThisVideoPlaying { pipMessageView diff --git a/Sources/SwiftyChat/Mock/MockMessages.swift b/Sources/SwiftyChat/Mock/MockMessages.swift index b487dc03..1c8993e2 100644 --- a/Sources/SwiftyChat/Mock/MockMessages.swift +++ b/Sources/SwiftyChat/Mock/MockMessages.swift @@ -9,15 +9,6 @@ import UIKit import Foundation -internal extension UIColor { - func image(_ size: CGSize = CGSize(width: 1, height: 1)) -> UIImage { - return UIGraphicsImageRenderer(size: size).image { rendererContext in - self.setFill() - rendererContext.fill(CGRect(origin: .zero, size: size)) - } - } -} - public struct MockMessages { public enum Kind { @@ -135,7 +126,6 @@ public struct MockMessages { public static var chatbot: ChatUserItem = .init( userName: "Chatbot", - // avatar: #imageLiteral(resourceName: "avatar") avatarURL: URL(string: "https://3.bp.blogspot.com/-vO7C5BPCaCQ/WigyjG6Q8lI/AAAAAAAAfyQ/1tobZMMwZ2YEI0zx5De7kD31znbUAth0gCLcBGAs/s200/TOMI_avatar_full.png") ) @@ -151,8 +141,9 @@ public struct MockMessages { public static func generateMessage(kind: MockMessages.Kind) -> ChatMessageItem { let randomUser = Self.randomUser + switch kind { - + case .Image: guard let url = URL(string: "https://picsum.photos/id/\(Int.random(in: 1...100))/400/300") else { fallthrough } return ChatMessageItem( @@ -174,7 +165,6 @@ public struct MockMessages { messageKind: .carousel([ CarouselRow( title: "Multiline Title", - // imageURL: URL(string:"https://picsum.photos/400/300"), imageURL: URL(string: "https://picsum.photos/id/1/400/200"), subtitle: "Multiline Subtitle, you do not believe me ?", buttons: [ @@ -183,7 +173,6 @@ public struct MockMessages { ), CarouselRow( title: "This one is really multiline", - // imageURL: URL(string:"https://picsum.photos/400/300"), imageURL: URL(string: "https://picsum.photos/id/2/400/200"), subtitle: "Multilinable Subtitle", buttons: [ @@ -218,8 +207,8 @@ public struct MockMessages { case .Contact: let contacts = [ ContactRow(displayName: "Enes Karaosman"), - ContactRow(displayName: "Adam Surname"), - ContactRow(displayName: "Name DummySurname") + ContactRow(displayName: "John Doe"), + ContactRow(displayName: "Serdar Bale") ] return ChatMessageItem( user: randomUser, @@ -229,10 +218,11 @@ public struct MockMessages { case .Video: let videoItem = VideoRow( - url: URL(string: "http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4")!, + url: URL(string: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4")!, placeholderImage: .remote(URL(string: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/images/BigBuckBunny.jpg")!), pictureInPicturePlayingMessage: "This video is playing in picture in picture." ) + return ChatMessageItem( user: randomUser, messageKind: .video(videoItem), @@ -244,12 +234,11 @@ public struct MockMessages { messageKind: .custom(Lorem.sentence()), isSender: randomUser == Self.sender ) - } } public static var randomMessageKind: MockMessages.Kind { - let allCases: [MockMessages.Kind] = [ + return [ .Image, .Text, .Text, .Text, .Contact, @@ -260,11 +249,10 @@ public struct MockMessages { .Video, .QuickReply, .Custom - ] - return allCases.randomElement()! + ].randomElement()! } public static func generatedMessages(count: Int = 30) -> [ChatMessageItem] { - return (1...count).map { _ in generateMessage(kind: randomMessageKind)} + (1...count).map { _ in generateMessage(kind: randomMessageKind)} } } diff --git a/Sources/SwiftyChat/VideoManager.swift b/Sources/SwiftyChat/VideoManager.swift index a6144234..687d0f6a 100644 --- a/Sources/SwiftyChat/VideoManager.swift +++ b/Sources/SwiftyChat/VideoManager.swift @@ -1,6 +1,6 @@ // // VideoManager.swift -// +// // // Created by Enes Karaosman on 9.11.2020. // @@ -11,18 +11,18 @@ import Foundation /// when placeHolder cell tapped, `message` parameter is set /// also this change is being observed in `PIPVideoCell`so activates video frame. internal final class VideoManager: ObservableObject { - + @Published var message: Message? @Published var isFullScreen = false var videoItem: VideoItem? { - if let message = message { - if case let ChatMessageKind.video(videoItem) = message.messageKind { - return videoItem - } + if let message = message, + case let ChatMessageKind.video(videoItem) = message.messageKind { + return videoItem } + return nil } - + func flushState() { message = nil isFullScreen = false diff --git a/SwiftyChatExample/SwiftyChatExample.xcodeproj/project.pbxproj b/SwiftyChatExample/SwiftyChatExample.xcodeproj/project.pbxproj index abe11c7f..421f9d99 100644 --- a/SwiftyChatExample/SwiftyChatExample.xcodeproj/project.pbxproj +++ b/SwiftyChatExample/SwiftyChatExample.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -125,8 +125,9 @@ F4D55CBA253F262F00DA8FF0 /* Project object */ = { isa = PBXProject; attributes = { + BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1200; - LastUpgradeCheck = 1200; + LastUpgradeCheck = 1520; TargetAttributes = { F4D55CC1253F262F00DA8FF0 = { CreatedOnToolsVersion = 12.0.1; @@ -231,6 +232,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -245,7 +247,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -292,6 +294,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -300,7 +303,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -320,7 +323,7 @@ DEVELOPMENT_TEAM = L9DND36YTU; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = SwiftyChatExample/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -342,7 +345,7 @@ DEVELOPMENT_TEAM = L9DND36YTU; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = SwiftyChatExample/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/SwiftyChatExample/SwiftyChatExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SwiftyChatExample/SwiftyChatExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5384cdec..8d00cca7 100644 --- a/SwiftyChatExample/SwiftyChatExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SwiftyChatExample/SwiftyChatExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,52 +1,32 @@ { - "object": { - "pins": [ - { - "package": "GSPlayer", - "repositoryURL": "https://github.com/wxxsw/GSPlayer.git", - "state": { - "branch": null, - "revision": "706fc28fcc8e33607993903eca82d9f6c7c3f6d2", - "version": "0.2.27" - } - }, - { - "package": "Kingfisher", - "repositoryURL": "https://github.com/onevcat/Kingfisher.git", - "state": { - "branch": null, - "revision": "add0a87ec4e31e2ca2a0b2085f0559201e4f5918", - "version": "7.10.1" - } - }, - { - "package": "SwiftUIEKtensions", - "repositoryURL": "https://github.com/EnesKaraosman/SwiftUIEKtensions.git", - "state": { - "branch": null, - "revision": "21421df3266c99b83992face9e1523c8f1acb924", - "version": "0.2.0" - } - }, - { - "package": "VideoPlayer", - "repositoryURL": "https://github.com/wxxsw/VideoPlayer.git", - "state": { - "branch": null, - "revision": "0a271928849292713daf471398a2a9185f28709c", - "version": "1.2.4" - } - }, - { - "package": "WrappingHStack", - "repositoryURL": "https://github.com/dkk/WrappingHStack.git", - "state": { - "branch": null, - "revision": "425d9488ba55f58f0b34498c64c054c77fc2a44b", - "version": "2.2.11" - } + "pins" : [ + { + "identity" : "kingfisher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/onevcat/Kingfisher.git", + "state" : { + "revision" : "5b92f029fab2cce44386d28588098b5be0824ef5", + "version" : "7.11.0" } - ] - }, - "version": 1 + }, + { + "identity" : "swiftuiektensions", + "kind" : "remoteSourceControl", + "location" : "https://github.com/EnesKaraosman/SwiftUIEKtensions.git", + "state" : { + "revision" : "21421df3266c99b83992face9e1523c8f1acb924", + "version" : "0.2.0" + } + }, + { + "identity" : "wrappinghstack", + "kind" : "remoteSourceControl", + "location" : "https://github.com/dkk/WrappingHStack.git", + "state" : { + "revision" : "425d9488ba55f58f0b34498c64c054c77fc2a44b", + "version" : "2.2.11" + } + } + ], + "version" : 2 } diff --git a/SwiftyChatExample/SwiftyChatExample/Info.plist b/SwiftyChatExample/SwiftyChatExample/Info.plist index becd96fc..7a638465 100644 --- a/SwiftyChatExample/SwiftyChatExample/Info.plist +++ b/SwiftyChatExample/SwiftyChatExample/Info.plist @@ -20,6 +20,11 @@ 1 LSRequiresIPhoneOS + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + UIApplicationSceneManifest UIApplicationSupportsMultipleScenes @@ -39,6 +44,10 @@ UIApplicationSupportsIndirectInputEvents + UIBackgroundModes + + audio + UILaunchStoryboardName LaunchScreen UIRequiredDeviceCapabilities @@ -51,11 +60,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait