Skip to content

Commit

Permalink
refactor: replace VideoPlayer dependency with UIKit+SwiftUI combinati…
Browse files Browse the repository at this point in the history
…on (#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
  • Loading branch information
EnesKaraosman authored Apr 9, 2024
1 parent 15ed0ee commit ef58abf
Show file tree
Hide file tree
Showing 18 changed files with 394 additions and 307 deletions.
10 changes: 9 additions & 1 deletion .github/workflows/swift.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
8 changes: 3 additions & 5 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
// 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

let package = Package(
name: "SwiftyChat",
platforms: [
.iOS(.v14),
.macOS(.v11)
.iOS(.v15),
.macOS(.v12)
],
products: [
.library(
Expand All @@ -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: [
Expand All @@ -29,7 +28,6 @@ let package = Package(
dependencies: [
.byName(name: "Kingfisher"),
.byName(name: "SwiftUIEKtensions"),
.byName(name: "VideoPlayer"),
.byName(name: "WrappingHStack")

],
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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. <br>
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 `<li>, <a>` (not like h1 or font based tag)
Expand Down
2 changes: 1 addition & 1 deletion Sources/SwiftyChat/ChatView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public struct ChatView<Message: ChatMessage, User: ChatUser>: View {
}
.frame(height: messageEditorHeight)
.padding(.bottom, 12)

PIPVideoCell<Message>()
}
.iOS { $0.keyboardAwarePadding() }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// PIPVideoCell.swift
//
//
//
// Created by Enes Karaosman on 9.11.2020.
//
Expand All @@ -15,25 +15,25 @@ internal extension CGSize {
}

internal struct PIPVideoCell<Message: ChatMessage>: View {

@EnvironmentObject var videoManager: VideoManager<Message>
@EnvironmentObject var model: DeviceOrientationInfo

@State private var cancellables: Set<AnyCancellable> = .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
} else {
return videoFrameWidth(in: size) / aspectRatio
}
}

private func videoFrameWidth(in size: CGSize) -> CGFloat {
if videoManager.isFullScreen {
return size.width
Expand All @@ -42,11 +42,11 @@ internal struct PIPVideoCell<Message: ChatMessage>: 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
Expand Down Expand Up @@ -81,16 +81,16 @@ internal struct PIPVideoCell<Message: ChatMessage>: 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))
Expand All @@ -101,7 +101,7 @@ internal struct PIPVideoCell<Message: ChatMessage>: View {
.animation(.linear(duration: 0.1))
.onAppear { rePositionVideoFrame(toCorner: .rightTop, in: geometry.size) }
.onAppear {

videoManager.$isFullScreen
.removeDuplicates()
.sink { fullScreen in
Expand All @@ -110,7 +110,7 @@ internal struct PIPVideoCell<Message: ChatMessage>: View {
}
}
.store(in: &cancellables)

model.$orientation
.removeDuplicates()
.sink(receiveValue: { _ in
Expand All @@ -127,13 +127,13 @@ internal struct PIPVideoCell<Message: ChatMessage>: View {
}
}
}

@ViewBuilder private var video: some View {
if let message = videoManager.message, let videoItem = videoManager.videoItem {
VideoPlayerContainer<Message>(media: videoItem, message: message)
CustomPlayerView(media: videoItem, message: message)
}
}

// MARK: - Drag Gesture
private func simpleDrag(in size: CGSize) -> some Gesture {
DragGesture()
Expand Down Expand Up @@ -161,5 +161,5 @@ internal struct PIPVideoCell<Message: ChatMessage>: View {
}
}
}
}
}
}
80 changes: 80 additions & 0 deletions Sources/SwiftyChat/Common/Video/PlayerViewModel.swift
Original file line number Diff line number Diff line change
@@ -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<AnyCancellable> = []
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)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import SwiftUI

struct CustomControlsView<Message: ChatMessage>: View {
@ObservedObject var playerVM: PlayerViewModel
@EnvironmentObject var videoManager: VideoManager<Message>

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()
}
}
}
Loading

0 comments on commit ef58abf

Please sign in to comment.