Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions App/App/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPhotoLibraryUsageDescription</key>
<string>동영상을 갤러리에 저장하기 위해 권한이 필요합니다.</string>
<key>UIUserInterfaceStyle</key>
<string>Dark</string>
<key>CFBundleDisplayName</key>
Expand Down
5 changes: 5 additions & 0 deletions App/App/SceneDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -136,5 +136,10 @@ extension SceneDelegate {
usecase: DIContainer.shared.resolve(type: EditVideoUseCaseInterface.self)
)
)

DIContainer.shared.register(
type: PreviewViewModel.self,
instance: PreviewViewModel(usecase: DIContainer.shared.resolve(type: EditVideoUseCaseInterface.self))
)
}
}
2 changes: 2 additions & 0 deletions Data/Data/SharingVideoRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public final class SharingVideoRepository: SharingVideoRepositoryInterface {

public let updatedSharedVideo = PassthroughSubject<SharedVideo, Never>()
public let isSynchronized = PassthroughSubject<Void, Never>()
public let startSynchronize = PassthroughSubject<Void, Never>()

public init(socketProvider: SharingVideoSocketProvidable) {
self.socketProvider = socketProvider
Expand All @@ -45,6 +46,7 @@ public final class SharingVideoRepository: SharingVideoRepositoryInterface {
guard let data = try? JSONDecoder().decode(SyncMessage.self, from: data) else { return }
switch data {
case .hash(let hashes):
startSynchronize.send(())
sendComparingResult(with: hashes, to: peerID.id)
case .hashMatch(let result):
synchronize(with: result, to: peerID.id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import Foundation
public protocol SharingVideoRepositoryInterface {
var updatedSharedVideo: PassthroughSubject<SharedVideo, Never> { get }
var isSynchronized: PassthroughSubject<Void, Never> { get }
var startSynchronize: PassthroughSubject<Void, Never> { get }

func shareVideo(url: URL, resourceName: String)
func broadcastHashes()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Foundation

public protocol EditVideoUseCaseInterface {
var editedVideos: PassthroughSubject<[Video], Never> { get }
var videos: [Video] { get }

func fetchVideos() async -> [Video]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import Foundation
public protocol SharingVideoUseCaseInterface {
var updatedSharedVideo: PassthroughSubject<SharedVideo, Never> { get }
var isSynchronized: PassthroughSubject<Void, Never> { get }
var startSynchronize: PassthroughSubject<Void, Never> { get }

func fetchVideos() -> [SharedVideo]
func shareVideo(_ url: URL, resourceName: String)
Expand Down
15 changes: 15 additions & 0 deletions Domain/UseCase/VideoUseCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,13 @@ public final class VideoUseCase {
private let editVideoRepository: EditVideoRepositoryInterface

public let isSynchronized = PassthroughSubject<Void, Never>()
public let startSynchronize = PassthroughSubject<Void, Never>()
public let updatedSharedVideo = PassthroughSubject<SharedVideo, Never>()
public let editedVideos = PassthroughSubject<[Video], Never>()

public var videos: [Video] {
editingVideos.values.sorted(by: { $0.index < $1.index })
}

public init(
sharingVideoRepository: SharingVideoRepositoryInterface,
Expand Down Expand Up @@ -106,6 +111,10 @@ private extension VideoUseCase {
.subscribe(isSynchronized)
.store(in: &cancellables)

sharingVideoRepository.startSynchronize
.subscribe(startSynchronize)
.store(in: &cancellables)

editVideoRepository.editedVideos
.sink(with: self) { (owner, videos) in
owner.editingVideos = videos
Expand All @@ -114,6 +123,12 @@ private extension VideoUseCase {
.store(in: &cancellables)
}

func updateEditingVideos(_ videos: [Video]) {
videos.forEach {
editingVideos[$0.url.path] = $0
}
}

func updatedVideo(url: URL, startTime: Double, endTime: Double) -> Video? {
guard let video = editingVideos.first(where: { $0.url.path == url.path })
else { return nil }
Expand Down
14 changes: 10 additions & 4 deletions Feature/Feature.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
/* End PBXFileReference section */

/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
FE8208012CED782500307694 /* Exceptions for "Feature" folder in "Feature" target */ = {
D95A3B062D01D7500007A71A /* Exceptions for "Feature" folder in "Feature" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
ConnectionView/ConnectionViewController.swift,
Expand All @@ -57,6 +57,7 @@
Helper/EventPublisher.swift,
Helper/RoundButton.swift,
"Helper/Sequence+AsyncCompactMap.swift",
Helper/Toast.swift,
"Helper/UIAlertController+Types.swift",
"Helper/UIColor+HexInitialization.swift",
"Helper/UIImage+Concat.swift",
Expand All @@ -65,6 +66,11 @@
"Helper/UIView+Extension.swift",
MainViewController.swift,
PresentationModel/VideoPresentationModel.swift,
PreviewView/PreviewViewController.swift,
PreviewView/ViewModel/PreviewViewInput.swift,
PreviewView/ViewModel/PreviewViewModel.swift,
PreviewView/ViewModel/PreviewViewOutput.swift,
ResultView/ResultViewController.swift,
SharedVideoEditView/SharedVideoEditViewController.swift,
SharedVideoEditView/VideoMerger/VideoMerger.swift,
SharedVideoEditView/View/CollectionView/VideoTimelineCollectionViewCell.swift,
Expand Down Expand Up @@ -102,10 +108,10 @@
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */

/* Begin PBXFileSystemSynchronizedRootGroup section */
FE6EF3F12CD8B71E005DC39D /* Feature */ = {
D95A3AC62D01D7500007A71A /* Feature */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
FE8208012CED782500307694 /* Exceptions for "Feature" folder in "Feature" target */,
D95A3B062D01D7500007A71A /* Exceptions for "Feature" folder in "Feature" target */,
);
path = Feature;
sourceTree = "<group>";
Expand All @@ -130,7 +136,7 @@
FE6EF3E62CD8B71E005DC39D = {
isa = PBXGroup;
children = (
FE6EF3F12CD8B71E005DC39D /* Feature */,
D95A3AC62D01D7500007A71A /* Feature */,
FE6EF4272CD8B843005DC39D /* Frameworks */,
FE6EF3F02CD8B71E005DC39D /* Products */,
);
Expand Down
56 changes: 56 additions & 0 deletions Feature/Feature/Helper/Toast.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//
// Toast.swift
// Feature
//
// Created by 디해 on 12/5/24.
//

import UIKit

extension UIView {
private static let toastTag = 9999

func showToast(message: String, duration: TimeInterval? = nil) {
if let existingToast = self.viewWithTag(UIView.toastTag) {
existingToast.removeFromSuperview()
}

let toastLabel = UILabel()
toastLabel.text = message
toastLabel.textColor = .white
toastLabel.backgroundColor = UIColor.black.withAlphaComponent(0.8)
toastLabel.textAlignment = .center
toastLabel.font = UIFont.systemFont(ofSize: 15)
toastLabel.numberOfLines = 0
toastLabel.tag = UIView.toastTag

let maxSize = CGSize(width: self.bounds.width - 40, height: self.bounds.height)
let textSize = toastLabel.sizeThatFits(maxSize)
toastLabel.frame = CGRect(
x: (self.bounds.width - textSize.width - 20) / 2,
y: self.bounds.height / 2 - textSize.height / 2,
width: textSize.width + 40,
height: textSize.height + 40
)
toastLabel.layer.cornerRadius = 20
toastLabel.layer.masksToBounds = true

self.addSubview(toastLabel)

if let duration = duration {
DispatchQueue.main.asyncAfter(deadline: .now() + duration) { [weak self] in
self?.hideToast()
}
}
}

func hideToast() {
if let toastLabel = self.viewWithTag(UIView.toastTag) {
UIView.animate(withDuration: 0.5, animations: {
toastLabel.alpha = 0.0
}) { _ in
toastLabel.removeFromSuperview()
}
}
}
}
122 changes: 122 additions & 0 deletions Feature/Feature/PreviewView/PreviewViewController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
//
// ResultViewController.swift
// Feature
//
// Created by 디해 on 12/5/24.
//

import AVFoundation
import Combine
import UIKit

public final class PreviewViewController: UIViewController {
private let videoView = VideoPlayerView()
private let saveButton = UIButton(type: .system)

private let viewModel: PreviewViewModel
private let input = PassthroughSubject<PreviewViewInput, Never>()
var cancellables = Set<AnyCancellable>()

public init(viewModel: PreviewViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}

public override func viewDidLoad() {
super.viewDidLoad()

setupUI()
setupViewModelBinding()
setupUIBinding()
}

public override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)

input.send(.loadPreview(size: videoView.frame.size))
}

@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

// MARK: - Binding
private extension PreviewViewController {
func setupViewModelBinding() {
let output = viewModel.transform(input.eraseToAnyPublisher())

output.receive(on: DispatchQueue.main)
.sink(with: self) { (owner, output) in
switch output {
case .loadedPreview(let playerItem):
owner.videoView.replaceVideo(playerItem: playerItem)
case .videoSaved:
owner.navigateToResult()
}
}
.store(in: &cancellables)
}

func setupUIBinding() {
saveButton.bs.tap
.sink(with: self) { owner, _ in
owner.input.send(.saveVideo)
}
.store(in: &cancellables)
}
}

// MARK: - UI Configure
private extension PreviewViewController {
func setupUI() {
setupViewAttributes()
setupViewHierarchies()
setupViewConstraints()
}

func setupViewAttributes() {
view.backgroundColor = .black
setupSaveButton()
}

func setupSaveButton() {
saveButton.backgroundColor = .white
saveButton.setTitle("저장", for: .normal)
saveButton.setTitleColor(.black, for: .normal)
saveButton.titleLabel?.font = .systemFont(ofSize: 20)
saveButton.layer.cornerRadius = 25
}

func setupViewHierarchies() {
view.addSubviews(
videoView,
saveButton
)
}

func setupViewConstraints() {
videoView.snp.makeConstraints { make in
make.top.equalToSuperview()
make.horizontalEdges.equalToSuperview()
make.height.equalTo(550)
}

saveButton.snp.makeConstraints { make in
make.centerX.equalToSuperview()
make.top.equalTo(videoView.snp.bottom).offset(30)
make.width.equalTo(160)
make.height.equalTo(50)
}
}
}

// MARK: - Private Methods
private extension PreviewViewController {
func navigateToResult() {
let resultViewController = ResultViewController()

navigationController?.pushViewController(resultViewController, animated: true)
}
}
15 changes: 15 additions & 0 deletions Feature/Feature/PreviewView/ViewModel/PreviewViewInput.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// ResultViewInput.swift
// Feature
//
// Created by 디해 on 12/5/24.
//

import Foundation

// MARK: - Input

enum PreviewViewInput {
case loadPreview(size: CGSize)
case saveVideo
}
Loading
Loading