Skip to content

Commit ffe54d7

Browse files
committed
update
1 parent b395c4b commit ffe54d7

File tree

3 files changed

+205
-36
lines changed

3 files changed

+205
-36
lines changed

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import PackageDescription
55

66
let package = Package(
77
name: "swiftui-loop-videoplayer",
8-
platforms: [.iOS(.v14)],
8+
platforms: [.iOS(.v14), .macOS(.v11), .tvOS(.v14)],
99
products: [
1010
// Products define the executables and libraries a package produces, and make them visible to other packages.
1111
.library(

Sources/swiftui-loop-videoplayer/LoopPlayerView.swift

Lines changed: 140 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,16 @@
66
//
77

88
import SwiftUI
9-
import UIKit
109
import AVKit
1110

1211
/// Player view for running a video in loop
13-
@available(iOS 14.0, *)
14-
public struct LoopPlayerView: UIViewRepresentable {
15-
12+
@available(iOS 14.0, macOS 11.0, tvOS 14.0, *)
13+
public struct LoopPlayerView: View {
14+
1615
/// Set of settings for video the player
17-
public let settings : Settings
16+
public let settings: Settings
1817

19-
// MARK: - Life circle
18+
// MARK: - Life cycle
2019

2120
/// Player initializer
2221
/// - Parameters:
@@ -26,17 +25,17 @@ public struct LoopPlayerView: UIViewRepresentable {
2625
/// - eText: Error message text if file is not found
2726
/// - eFontSize: Size of the error text
2827
public init(
29-
fileName: String,
30-
ext: String = "mp4",
31-
gravity: AVLayerVideoGravity = .resizeAspect,
32-
eColor : Color = .accentColor,
33-
eFontSize : CGFloat = 17.0
28+
fileName: String,
29+
ext: String = "mp4",
30+
gravity: AVLayerVideoGravity = .resizeAspect,
31+
eColor: Color = .accentColor,
32+
eFontSize: CGFloat = 17.0
3433
) {
35-
settings = Settings{
34+
settings = Settings {
3635
FileName(fileName)
3736
Ext(ext)
3837
Gravity(gravity)
39-
ErrorGroup{
38+
ErrorGroup {
4039
EColor(eColor)
4140
EFontSize(eFontSize)
4241
}
@@ -45,42 +44,55 @@ public struct LoopPlayerView: UIViewRepresentable {
4544

4645
/// Player initializer in a declarative way
4746
/// - Parameter settings: Set of settings
48-
public init(_ settings : () -> Settings) {
47+
public init(_ settings: () -> Settings) {
4948
self.settings = settings()
5049
}
51-
50+
5251
// MARK: - API
5352

54-
/// Inherited from UIViewRepresentable
55-
public func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<LoopPlayerView>) {
53+
public var body: some View {
54+
#if os(iOS) || os(tvOS)
55+
LoopPlayerViewRepresentableIOS(settings: settings)
56+
.frame(maxWidth: .infinity, maxHeight: .infinity)
57+
#elseif os(macOS)
58+
LoopPlayerViewRepresentableMacOS(settings: settings)
59+
.frame(maxWidth: .infinity, maxHeight: .infinity)
60+
#endif
5661
}
62+
}
5763

58-
/// - Parameter context: Contains details about the current state of the system
59-
/// - Returns: View
60-
public func makeUIView(context: Context) -> UIView {
61-
64+
// MARK: - Representable for iOS and tvOS
65+
66+
#if os(iOS) || os(tvOS)
67+
@available(iOS 14.0, tvOS 14.0, *)
68+
struct LoopPlayerViewRepresentableIOS: UIViewRepresentable {
69+
70+
let settings: Settings
71+
72+
func makeUIView(context: Context) -> UIView {
6273
let name = settings.name
6374
let ext = settings.ext
6475
let gravity = settings.gravity
6576
let color = settings.errorColor
6677
let fontSize = settings.errorFontSize
6778

68-
guard settings.areUnique else{
69-
return errorTpl(.settingsNotUnique, color, fontSize)
79+
guard settings.areUnique else {
80+
return errorTpliOS(.settingsNotUnique, color, fontSize)
7081
}
7182

72-
guard let view = LoopingPlayerUIView(name, width: ext, gravity: gravity) else{
73-
return errorTpl(.fileNotFound(name), color, fontSize)
83+
guard let view = LoopingPlayerUIView(name, width: ext, gravity: gravity) else {
84+
return errorTpliOS(.fileNotFound(name), color, fontSize)
85+
}
86+
return view
7487
}
75-
return view
88+
89+
func updateUIView(_ uiView: UIView, context: Context) {
7690
}
7791
}
7892

93+
// MARK: - Helpers for iOS and tvOS
7994

80-
// MARK: - Helpers
81-
82-
/// https://stackoverflow.com/questions/12591192/center-text-vertically-in-a-uitextview
83-
fileprivate class ErrorMsgTextView: UITextView {
95+
fileprivate class ErrorMsgTextViewIOS: UITextView {
8496

8597
override var contentSize: CGSize {
8698
didSet {
@@ -91,13 +103,108 @@ fileprivate class ErrorMsgTextView: UITextView {
91103
}
92104
}
93105

94-
/// - Returns: Error view
95-
fileprivate func errorTpl(_ error : VPErrors,_ color : Color, _ fontSize: CGFloat) -> ErrorMsgTextView{
96-
let textView = ErrorMsgTextView()
106+
fileprivate func errorTpliOS(_ error: VPErrors, _ color: Color, _ fontSize: CGFloat) -> UIView {
107+
let textView = ErrorMsgTextViewIOS()
97108
textView.backgroundColor = .clear
98109
textView.text = error.description
99110
textView.textAlignment = .center
100111
textView.font = UIFont.systemFont(ofSize: fontSize)
101112
textView.textColor = UIColor(color)
102113
return textView
103114
}
115+
116+
#endif
117+
118+
// MARK: - Representable for macOS
119+
120+
#if os(macOS)
121+
@available(macOS 11.0, *)
122+
struct LoopPlayerViewRepresentableMacOS: NSViewRepresentable {
123+
124+
let settings: Settings
125+
126+
func makeNSView(context: Context) -> NSView {
127+
let name = settings.name
128+
let ext = settings.ext
129+
let gravity = settings.gravity
130+
let color = settings.errorColor
131+
let fontSize = settings.errorFontSize
132+
133+
guard settings.areUnique else {
134+
return errorTplmacOS(.settingsNotUnique, color, fontSize)
135+
}
136+
137+
guard let view = LoopingPlayerNSView(name, width: ext, gravity: gravity) else {
138+
return errorTplmacOS(.fileNotFound(name), color, fontSize)
139+
}
140+
return view
141+
}
142+
143+
func updateNSView(_ nsView: NSView, context: Context) {
144+
}
145+
}
146+
147+
// MARK: - Helpers for macOS
148+
149+
fileprivate class ErrorMsgTextViewMacOS: NSTextView {
150+
151+
override var intrinsicContentSize: NSSize {
152+
return NSSize(width: NSView.noIntrinsicMetric, height: NSView.noIntrinsicMetric)
153+
}
154+
155+
override func viewDidMoveToSuperview() {
156+
super.viewDidMoveToSuperview()
157+
guard let superview = superview else { return }
158+
159+
translatesAutoresizingMaskIntoConstraints = false
160+
161+
NSLayoutConstraint.activate([
162+
leadingAnchor.constraint(equalTo: superview.leadingAnchor, constant: 10),
163+
trailingAnchor.constraint(equalTo: superview.trailingAnchor, constant: -10),
164+
topAnchor.constraint(equalTo: superview.topAnchor),
165+
bottomAnchor.constraint(equalTo: superview.bottomAnchor)
166+
])
167+
}
168+
169+
override func layout() {
170+
super.layout()
171+
172+
guard let layoutManager = layoutManager, let textContainer = textContainer else {
173+
return
174+
}
175+
176+
let textHeight = layoutManager.usedRect(for: textContainer).size.height
177+
let containerHeight = bounds.size.height
178+
let verticalInset = max(0, (containerHeight - textHeight) / 2)
179+
180+
textContainerInset = NSSize(width: 0, height: verticalInset)
181+
}
182+
}
183+
184+
fileprivate func errorTplmacOS(_ error: VPErrors, _ color: Color, _ fontSize: CGFloat) -> NSView {
185+
let textView = ErrorMsgTextViewMacOS()
186+
textView.isEditable = false
187+
textView.isSelectable = false
188+
textView.drawsBackground = false
189+
textView.string = error.description
190+
textView.alignment = .center
191+
textView.font = NSFont.systemFont(ofSize: fontSize)
192+
textView.textColor = NSColor(color)
193+
194+
let containerView = NSView()
195+
containerView.addSubview(textView)
196+
197+
textView.translatesAutoresizingMaskIntoConstraints = false
198+
199+
// Center textView in containerView with padding
200+
NSLayoutConstraint.activate([
201+
textView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
202+
textView.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
203+
textView.leadingAnchor.constraint(greaterThanOrEqualTo: containerView.leadingAnchor, constant: 10),
204+
textView.trailingAnchor.constraint(lessThanOrEqualTo: containerView.trailingAnchor, constant: -10)
205+
])
206+
207+
return containerView
208+
}
209+
210+
#endif

Sources/swiftui-loop-videoplayer/view/LoopingPlayerUIView.swift

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
//
22
// LoopingPlayerUIView.swift
3-
//
3+
//
44
//
55
// Created by Igor on 10.02.2023.
66
//
77

8-
import UIKit
98
import AVKit
109

10+
11+
#if canImport(UIKit) && os(tvOS)
12+
import UIKit
13+
1114
@available(iOS 14.0, *)
1215
class LoopingPlayerUIView: UIView {
1316

@@ -60,3 +63,62 @@ class LoopingPlayerUIView: UIView {
6063
playerLayer.frame = bounds
6164
}
6265
}
66+
67+
#elseif canImport(Cocoa)
68+
import Cocoa
69+
70+
@available(macOS 11.0, *)
71+
class LoopingPlayerNSView: NSView {
72+
73+
/// An object that presents the visual contents of a player object
74+
private let playerLayer = AVPlayerLayer()
75+
76+
/// An object that loops media content using a queue player
77+
private var playerLooper: AVPlayerLooper?
78+
79+
required init?(coder: NSCoder) {
80+
fatalError("init(coder:) has not been implemented")
81+
}
82+
83+
/// - Parameters:
84+
/// - name: Name of the video to play
85+
/// - ext: Video extension
86+
/// - gravity: A structure that defines how a layer displays a player’s visual content within the layer’s bounds
87+
init?(_ name: String, width ext: String, gravity: AVLayerVideoGravity) {
88+
89+
/// Load the resource
90+
guard let fileUrl = Bundle.main.url(forResource: name, withExtension: ext) else{
91+
return nil
92+
}
93+
94+
let asset = AVAsset(url: fileUrl)
95+
96+
let item = AVPlayerItem(asset: asset)
97+
98+
/// Setup the player
99+
let player = AVQueuePlayer()
100+
player.isMuted = true
101+
playerLayer.player = player
102+
playerLayer.videoGravity = gravity
103+
playerLayer.backgroundColor = NSColor.clear.cgColor
104+
105+
super.init(frame: .zero)
106+
107+
/// Create a new player looper with the queue player and template item
108+
playerLooper = AVPlayerLooper(player: player, templateItem: item)
109+
110+
/// Start the movie
111+
player.play()
112+
113+
layer = CALayer()
114+
layer?.addSublayer(playerLayer)
115+
wantsLayer = true
116+
}
117+
118+
/// Called automatically when the view's bounds change
119+
override func layout() {
120+
super.layout()
121+
playerLayer.frame = bounds
122+
}
123+
}
124+
#endif

0 commit comments

Comments
 (0)