I have a videPlayerView which has ContainerView on top of it to show activityIndicatorView. The view gets loaded when i select an item from a collectionView inside a cell. My question is how to remove this view using the cancelButton, i tried removeFromSuperview() for both the container and the playerView but the app crashes with this error
AQDefaultDevice (1): skipping input stream 0 0 0x0
Here is the code:
class VideoPlayerView: UIView {
var videoUrl: String!
var uuidd: String!
let activityIndicatorView: UIActivityIndicatorView = {
let aiv = UIActivityIndicatorView(activityIndicatorStyle: .whiteLarge)
aiv.translatesAutoresizingMaskIntoConstraints = false
aiv.startAnimating()
return aiv
}()
lazy var controlsContainerView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(white: 0, alpha: 1)
view.isUserInteractionEnabled = true
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handlezoomout)))
return view
}()
func handlezoomout(hh: UITapGestureRecognizer){
print("n3me")
}
lazy var cancelButton: UIButton = {
let cancelButton = UIButton()
cancelButton.setImage(#imageLiteral(resourceName: "cancel"), for: UIControlState())
cancelButton.addTarget(self, action: #selector(cancel), for: .touchUpInside)
return cancelButton
}()
func cancel() {
controlsContainerView.removeFromSuperview()
let video = VideoPlayerView()
video.removeFromSuperview()
}
lazy var pausePlayButton: UIButton = {
let button = UIButton(type: .system)
let image = UIImage(named: "pause")
button.setImage(image, for: UIControlState())
button.translatesAutoresizingMaskIntoConstraints = false
button.tintColor = .white
button.isHidden = true
button.addTarget(self, action: #selector(handlePause), for: .touchUpInside)
return button
}()
var isPlaying = false
func handlePause() {
if isPlaying {
player?.pause()
pausePlayButton.setImage(UIImage(named: "play"), for: UIControlState())
} else {
player?.play()
pausePlayButton.setImage(UIImage(named: "pause"), for: UIControlState())
}
isPlaying = !isPlaying
}
override init(frame: CGRect) {
super.init(frame: frame)
setUpPlayerView()
controlsContainerView.frame = frame
addSubview(controlsContainerView)
cancelButton.frame = CGRect(x: 16.0, y: 20.0, width: 30.0, height: 30.0)
controlsContainerView.addSubview(cancelButton)
controlsContainerView.addSubview(activityIndicatorView)
activityIndicatorView.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
activityIndicatorView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
controlsContainerView.addSubview(pausePlayButton)
pausePlayButton.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
pausePlayButton.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
pausePlayButton.widthAnchor.constraint(equalToConstant: 50).isActive = true
pausePlayButton.heightAnchor.constraint(equalToConstant: 50).isActive = true
backgroundColor = UIColor.black
}
var player: AVPlayer?
fileprivate func setUpPlayerView() {
let postQuery = PFQuery(className: "posts")
postQuery.whereKey("uuid", equalTo: PostUuidGlobalVariable.postuuid.last!)
postQuery.getFirstObjectInBackground { (object, error) in
if (error == nil && object != nil) {
let videoFile = object!["video"] as! PFFile
if let url = URL (string: videoFile.url!) {
self.player = AVPlayer(url: url)
let playerLayer = AVPlayerLayer(player: self.player)
self.layer.addSublayer(playerLayer)
playerLayer.frame = self.frame
self.player?.play()
self.player?.isMuted = false
self.player?.addObserver(self, forKeyPath: "currentItem.loadedTimeRanges", options: .new, context: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.playerDidFinishPlaying(note:)),
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: self.player?.currentItem)
}
}
}
}
func playerDidFinishPlaying(note: NSNotification) {
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
//this is when the player is ready and rendering frames
if keyPath == "currentItem.loadedTimeRanges" {
activityIndicatorView.stopAnimating()
pausePlayButton.isHidden = false
isPlaying = true
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class VideoLauncher: NSObject {
func showVideoPlayer() {
if let keyWindow = UIApplication.shared.keyWindow {
let view = UIView(frame: keyWindow.frame)
view.isUserInteractionEnabled = true
view.backgroundColor = UIColor.white
view.frame = CGRect(x: keyWindow.frame.width - 10, y: keyWindow.frame.height - 10, width: 10, height: 10)
let height = keyWindow.frame.height
let videoPlayerFrame = CGRect(x: 0, y: 0, width: keyWindow.frame.width, height: height)
let videoPlayerView = VideoPlayerView(frame: videoPlayerFrame)
view.addSubview(videoPlayerView)
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
view.frame = keyWindow.frame
}, completion: { (completedAnimation) in
//maybe we'll do something here later...
UIApplication.shared.isStatusBarHidden = true
})
keyWindow.addSubview(view)
}
}
}
There is a chance this is related to you changing the user interface outside of the main thread.
From UIView documents
'Threading Considerations
Manipulations to your application’s user interface must occur on the main thread. Thus, you should always call the methods of the
UIView
class from code running in the main thread of your application. The only time this may not be strictly necessary is when creating the view object itself, but all other manipulations should occur on the main thread.'
Also your cancel function creates a new video player view and then tries to remove it from it parent which doesn't look correct.
Your cancel callback should probably be as follows
func cancel() {
DispatchQueue.main.async { [unowned self] in
// to remove controls
self.controlsContainerView.removeFromSuperview()
// to remove video player view
self.view.removeFromSuperview()
}
}
Related
I want to create a custom alert via code. This code below is working, but I Need help on some questions about passing data and retain cycles
is it right my usage of [weak self] ?
I'd like to avoid Delegate Pattern, so my plan is to pass an action as handler, this should keep Controller clean and make code more reusable. Is mine a proper solution?
In my mind, a view should not be "auto removing" but its parent should remove it, so I'd like to pass a reference to the parent controller in order to comunicate via completion, but it seems to create a retain circle (deinit never called), so I'm doing this:
self?.removeFromSuperview() //works, but not sure it is right
//self?.parentController.removeFromParent() //never calls deinit
I had a problem passing closure as parameter inside an init with cgrect as parameter. Is there a proper way other than this solution to handle that?
required init(refersTo: UIViewController, comp: #escaping () -> Void) {
myTransmittedCompletion = comp
self.parentController = refersTo
super.init(frame: CGRect.zero)
}
I call my alert this way
#IBAction func centralButton(_ sender: UIButton) {
let alert = MyAlertInCode(refersTo: self, comp: testPerCOmpletion)
self.view.addSubview(alert)
}
func testPerCOmpletion() {
print("completion")
}
my custom class
class MyAlertInCode: UIView {
let myTransmittedCompletion: () -> Void
let parentController: UIViewController
private let myTitleLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.numberOfLines = 0
v.text = "very long long long logn title"
v.textAlignment = .center
v.font = UIFont.systemFont(ofSize: 28, weight: .bold)
return v
}()
private let mySubtTitleLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.numberOfLines = 0
v.text = "very long long long logn title"
v.textAlignment = .center
v.font = UIFont.systemFont(ofSize: 20, weight: .bold)
return v
}()
private let myButton: UIButton = {
let v = UIButton()
v.translatesAutoresizingMaskIntoConstraints = false
v.setTitle("Button", for: .normal)
v.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .bold)
v.setTitleColor(.systemBlue, for: .normal)
return v
}()
//white panel of the alert
private let container: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .white
v.layer.cornerRadius = 24
v.backgroundColor = .purple
return v
}()
private lazy var stack: UIStackView = {
let v = UIStackView(arrangedSubviews: [myTitleLabel, mySubtTitleLabel, myButton])
v.translatesAutoresizingMaskIntoConstraints = false
v.axis = .vertical
v.spacing = 10
v.distribution = .fillEqually
v.backgroundColor = .green
return v
}()
required init(refersTo: UIViewController, comp: #escaping () -> Void) {
myTransmittedCompletion = comp
self.parentController = refersTo
super.init(frame: CGRect.zero)
myButton.addTarget(self, action: #selector(methodInsideAlertClass), for: .touchUpInside)
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(animateOut)))
self.backgroundColor = UIColor.gray.withAlphaComponent(0.6)
#warning("UIScreen.main.bounds //deprecated in the future, at 14-ott-2022")
// self.frame = UIScreen.main.bounds //deprecated in the future, at 14-ott-2022
guard let windowBoundsFromIOS13 = UIApplication.shared.currentUIWindow()?.rootViewController?.view.bounds else {return}
self.frame = windowBoundsFromIOS13
self.addSubview(container)
NSLayoutConstraint.activate([
container.centerYAnchor.constraint(equalTo: self.centerYAnchor),
container.centerXAnchor.constraint(equalTo: self.centerXAnchor),
container.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 0.7),
container.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 0.5),
])
container.addSubview(stack)
NSLayoutConstraint.activate([
stack.heightAnchor.constraint(equalTo: container.heightAnchor, multiplier: 0.6),
stack.centerYAnchor.constraint(equalTo: container.centerYAnchor),
stack.leadingAnchor.constraint(equalTo: container.leadingAnchor),
stack.trailingAnchor.constraint(equalTo: container.trailingAnchor),
])
animateIn()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
print("❌")
}
//MARK: methods
#objc private func methodInsideAlertClass() {
print("methodInsideAlertClass tapped")
}
#objc private func animateOut() {
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 1, options: .curveEaseIn) {
self.container.transform = CGAffineTransform(translationX: 0, y: -self.frame.height)
self.alpha = 0
} completion: { [weak self] isCompleted in
if isCompleted {
self?.myTransmittedCompletion()
self?.removeFromSuperview() // shouldn't be removed by parent view?
// self?.parentController.removeFromParent() //never calls deinit
}
}
}
#objc private func animateIn() {
self.container.transform = CGAffineTransform(translationX: 0, y: -self.frame.height)
self.alpha = 1
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 1, options: .curveEaseIn) {
self.container.transform = .identity
self.alpha = 1
}
}
}
since cannot use Windows:
public extension UIApplication {
func currentUIWindow() -> UIWindow? {
let connectedScenes = UIApplication.shared.connectedScenes
.filter { $0.activationState == .foregroundActive }
.compactMap { $0 as? UIWindowScene }
let window = connectedScenes.first?
.windows
.first { $0.isKeyWindow }
return window
}
}
I have a high CPU level whenever I push a new ViewController from the navigationController.
I used the Time Profiler tool and it turns out that the issue that is causing this CPU overuse is related to a ViewController I embedded in the rootViewController.
The ViewController I'm talking about is as follows:
class QuoteGeneratorController : UIViewController {
let interactiveShowTextView : UITextView = {
let textView = UITextView()
textView.font = UIFont(name: "avenir-black", size: 35)
textView.textColor = .white
textView.textAlignment = .left
textView.textContainer.maximumNumberOfLines = 3
textView.backgroundColor = .clear
textView.isEditable = false
textView.isSelectable = false
textView.isUserInteractionEnabled = false
return textView
}()
let progressBar : UIProgressView = {
let progressView = UIProgressView(progressViewStyle: .bar)
progressView.trackTintColor = UIColor.clear
progressView.tintColor = UIColor.white
return progressView
}()
let quotesArray = ["Hello", "Hello2", "Hello3"]
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
print("I'm called")
view.subviews.forEach { subview in
subview.layer.masksToBounds = true
subview.layer.cornerRadius = progressBar.frame.height / 2.0
}
}
override func viewDidAppear(_ animated: Bool) {
setUpAnimation()
}
override func viewDidLoad() {
setUpUI()
setUpAnimation()
}
fileprivate func setUpUI(){
view.addSubview(interactiveShowTextView)
view.addSubview(progressBar)
interactiveShowTextView.anchor(top: view.safeAreaLayoutGuide.topAnchor, leading: view.leadingAnchor, bottom: nil, trailing: view.trailingAnchor)
interactiveShowTextView.heightAnchor.constraint(equalToConstant: 150).isActive = true
progressBar.anchor(top: interactiveShowTextView.bottomAnchor, leading: interactiveShowTextView.leadingAnchor, bottom: nil, trailing: nil, padding: .init(top: 10, left: 0, bottom: 0, right: 0), size: .init(width: 70, height: 9))
}
var iterator : Int = 0
fileprivate func setUpAnimation(){
switch iterator {
case 0:
animateAndIterate()
case 1:
animateAndIterate()
case 2:
animateAndIterate()
default:
self.iterator = 0
self.setUpAnimation()
}
}
fileprivate func animateAndIterate(){
UIView.animate(withDuration: 0.0, animations: {
self.progressBar.layoutIfNeeded()
}, completion: { finished in
self.progressBar.progress = 1.0
self.interactiveShowTextView.text = self.quotesArray[self.iterator]
self.interactiveShowTextView.fadeIn()
UIView.animate(withDuration: 3, delay: 0.0, options: [.curveLinear], animations: {
self.progressBar.layoutIfNeeded()
self.perform(#selector(self.afterAnimation), with: nil, afterDelay: 2.5)
}, completion: { finished in
self.interactiveShowTextView.fadeOut()
self.progressBar.progress = 0
self.iterator = self.iterator + 1
self.setUpAnimation()
})
})
}
#objc func afterAnimation() {
self.interactiveShowTextView.fadeOut()
}
}
I actually don't know what could have caused the issue and since I'm new to time profiler I though that some of you have encountered the same process through development.
In Time Profiler I'm getting this indications:
When using completion blocks it's recommended to use weak references to the result. Otherwise the ARC will count every reference you used inside those block.
Use closures like this:
view.subviews.forEach { [weak self] subview in
subview.layer.masksToBounds = true
subview.layer.cornerRadius = progressBar.frame.height / 2.0
}
// (...)
fileprivate func animateAndIterate() {
UIView.animate(withDuration: 0.0,
animations: { [weak self] in
self?.progressBar.layoutIfNeeded()
}, completion: { [weak self] finished in
self?.progressBar.progress = 1.0
self?.interactiveShowTextView.text = self.quotesArray[self.iterator]
self?.interactiveShowTextView.fadeIn()
UIView.animate(withDuration: 3,
delay: 0.0,
options: [.curveLinear],
animations: { [weak self] in
self?.progressBar.layoutIfNeeded()
self?.perform(#selector(self.afterAnimation),
with: nil, afterDelay: 2.5)
}, completion: { finished in
self?.interactiveShowTextView.fadeOut()
self?.progressBar.progress = 0
self?.iterator = self.iterator + 1
self?.setUpAnimation()
})
})
}
pic of database
I am trying to show a label with the user's status on their profile page. after logging in the user gets presented with a VC that has a side menu. on that side menu is a "profile" option. once choosing this they go to their profile controller. right now i simply need to search users/current uid/ "MembershipStatus" and present this result into a label called "welcomeLabel".
I am returning nul
import UIKit
import Firebase
class NonMemberProfileController: UIViewController {
// MARK: - Properties
var welcomeLabel: UILabel = {
let label = UILabel()
label.textColor = .white
label.font = UIFont.systemFont(ofSize: 28)
label.translatesAutoresizingMaskIntoConstraints = false
label.alpha = 0
return label
}()
// MARK: - Init
override func viewDidLoad()
{
super.viewDidLoad()
authenticateUserAndConfigureView()
}
func loadUserData()
{
guard let uid = Auth.auth().currentUser?.uid else {return}
//.child("MembershipStatus")
Database.database().reference().child("users").child(uid).observeSingleEvent(of: .value) {
(snapshot) in
if snapshot.hasChild("MembershipStatus"){
print("true we have bingo")
} else {
print("no bueno")
dump(snapshot)
}
guard let status = snapshot.value as? String else { return }
self.welcomeLabel.text = "Welcome, \(status)"
print("this is lkjfdskjklsfad" + status)
UIView.animate(withDuration: 0.5, animations: {
self.welcomeLabel.alpha = 1
})
}
}
func authenticateUserAndConfigureView(){
if Auth.auth().currentUser == nil {
DispatchQueue.main.async {
let navController = UINavigationController(rootViewController: LoginViewController())
navController.navigationBar.barStyle = .black
self.present(navController, animated: true, completion: nil)
}
} else {
configureNavigationBar()
loadUserData()
}
}
// MARK: - Selectors
#objc func handleDismiss() {
dismiss(animated: true, completion: nil)
}
// MARK: - Helper Functions
func configureNavigationBar() {
view.backgroundColor = UIColor.lightGray
navigationItem.title = "Profile"
navigationController?.navigationBar.barTintColor = .darkGray
navigationController?.navigationBar.barStyle = .black
navigationItem.leftBarButtonItem = UIBarButtonItem(image: #imageLiteral(resourceName: "Home_2x").withRenderingMode(.alwaysOriginal), style: .plain, target: self, action: #selector(handleDismiss))
navigationItem.rightBarButtonItem = UIBarButtonItem(image: #imageLiteral(resourceName: "baseline_settings_white_24dp").withRenderingMode(.alwaysOriginal), style: .plain, target: self, action: #selector(handleDismiss))
view.addSubview(welcomeLabel)
welcomeLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
welcomeLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
}
}
You are using Cloud Firestore for data storage but your code is reading data form RealTime Database. You have to read data like this:
let userRef = Firestore.firestore().collection("users").document(uid)
userRef.getDocument { (documentSnapshot, error) in guard
let document = documentSnapshot?.data() else {
print(error)
return
}
print(document)
}
I am resizing a cell frame on the iPhone X to embed an instance of AVPlayerController. When i change orientation from portrait to landscape the frame size seems to change.
I end up with the controls (full screen + volume) overlapping the header and title.
Would you recommend a solution other than :
self.frame.insetBy
Here is a demo of how it looks :
iphone x demo
import UIKit
import AVKit
class VGMediaPlayerCell: VGBaseCell {
let statusBarHeight: CGFloat = 20
let contentOffset: CGFloat = 50
static let vgReuseIdentifier = "VGMediaPlayerCell"
static var playerIsPlaying: Bool = false
var toggleHeaderVisibility: Bool = false
public weak var delegate: VGMediaPlayerCellDelegate?
var moviePlayerController = AVPlayerViewController()
var waitingIndicator = UIActivityIndicatorView(style: UIActivityIndicatorView.Style.whiteLarge)
var containerView = UIView()
var messageLabel = UILabel()
var needAutoPlay: Bool = false
var isLoaded: Bool = false
var asset: AVAsset?
var isReadyForDisplayObserver: NSKeyValueObservation?
var content: VGContent?
let deviceOrientation = UIDevice.current.orientation
//player settings
#objc var player: AVPlayer?
var PlayerViewConroller: AVPlayerViewController?
override init(frame: CGRect) {
super.init(frame: frame)
setupWaitingIndicator()
setupMessageLabel()
isReadyForDisplayObserver = moviePlayerController.observe(\.isReadyForDisplay) { [weak self] (_, _) in
guard let `self` = self else {
return
}
// When the first frame of the video is loaded, we dismiss the waiting indicator.
DispatchQueue.main.async {
if self.moviePlayerController.isReadyForDisplay {
self.waitingStateActive(isActive: false)
}
}
}
}
override func prepareForReuse() {
super.prepareForReuse()
self.isLoaded = false
needAutoPlay = false
moviePlayerController.player = nil
content = nil
asset = nil
player = nil
contextualLabel.font = nil
messageLabel.text = nil
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - View creation
func setupContainerView() {
addSubview(containerView)
containerView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
containerView.leftAnchor.constraint(equalTo: leftAnchor),
containerView.rightAnchor.constraint(equalTo: rightAnchor),
containerView.topAnchor.constraint(equalTo: topAnchor),
containerView.bottomAnchor.constraint(equalTo: bottomAnchor)
])
}
func setupMessageLabel() {
addSubview(messageLabel)
messageLabel.textAlignment = .center
messageLabel.textColor = .white
messageLabel.numberOfLines = 2
messageLabel.isHidden = true
messageLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
messageLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: 10),
messageLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: -10),
messageLabel.heightAnchor.constraint(equalToConstant: 50),
messageLabel.centerYAnchor.constraint(equalTo: centerYAnchor)
])
}
func setupWaitingIndicator() {
addSubview(waitingIndicator)
waitingIndicator.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
waitingIndicator.centerXAnchor.constraint(equalTo: centerXAnchor),
waitingIndicator.centerYAnchor.constraint(equalTo: centerYAnchor),
waitingIndicator.widthAnchor.constraint(equalToConstant: 100),
waitingIndicator.heightAnchor.constraint(equalToConstant: 100)
])
}
// MARK: - Utils
func configurePlayer(with viewModel: VGMediaPlayerViewModel) {
//to update message label + loader
updateUI(with: viewModel)
if viewModel.error == ErrorMessage.noNetwork.rawValue {
self.stop()
}
// Create a new AVPlayer and AVPlayerLayer
guard let url = URL(string: viewModel.content?.contentURL ?? "") else { return }
self.player = AVPlayer(url: url)
// We want video controls so we need an AVPlayerViewController
PlayerViewConroller = AVPlayerViewController()
PlayerViewConroller?.player = player
PlayerViewConroller?.videoGravity = AVLayerVideoGravity.resizeAspect
insertSubview(avPlayerViewConroller!.view, at: 0)
PlayerViewConroller!.view.topAnchor.constraint(equalTo: topAnchor).isActive = true
PlayerViewConroller!.view.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
PlayerViewConroller!.view.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
PlayerViewConroller!.view.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
self.bringSubviewToFront((avPlayerViewConroller?.view!)!)
if #available(iOS 10.0, *) {
self.player?.automaticallyWaitsToMinimizeStalling = false
}
guard let asset = viewModel.avAsset else { return }
if !asset.isPlayable {
DispatchQueue.main.async {
self.waitingStateActive(isActive: false)
self.displayError(message: ErrorMessage.noPreview.rawValue)
}
}
DispatchQueue.main.async {
// Create a new AVAsset from the URL
let videoAsset = AVAsset(url: url)
// // Now we need an AVPlayerItem to pass to the AVPlayer
let videoPlayerItem = AVPlayerItem(asset: videoAsset)
// // Finally, we set this as the current AVPlayer item
self.player?.replaceCurrentItem(with: videoPlayerItem)
if self.needAutoPlay {
self.player?.play()
}
self.isLoaded = true
}
//custom insets per device orientation
// regular from for iphone 8 and downwards
// custom frame for iphone X and upwards
if UIDevice().userInterfaceIdiom == .phone {
switch UIScreen.main.nativeBounds.height {
//iPhone 5 or 5S or 5C, iPhone 6/6S/7/8, iPhone 6+/6S+/7+/8+
case 1136, 1334, 1920, 2208:
PlayerViewConroller?.view.frame = self.frame
//iPhone X, Xs, iPhone Xs Max, iPhone Xr
case 2436, 2688, 1792:
if UIApplication.shared.statusBarOrientation.isPortrait {
PlayerViewConroller?.view.frame = self.frame.insetBy(dx: 0.0, dy: 50.0)
} else if deviceOrientation == .landscapeLeft || deviceOrientation == .landscapeRight {
PlayerViewConroller?.view.frame = self.frame.insetBy(dx: 30.0, dy: 30.0)
}
default: break
}
} else {
//for the iPad
PlayerViewConroller?.view.frame = self.frame
}
//Add observer on keypath rate to monitor player's playing status
if self.toggleHeaderVisibility == true {
if UIDevice().userInterfaceIdiom == .phone {
switch UIScreen.main.nativeBounds.height {
case 2436, 2688, 1792:
player?.addObserver(self, forKeyPath: "rate", options: [.old, .new], context: nil)
default : break
}
}
}
player?.addObserver(self, forKeyPath: "rate", options: [.old, .new], context: nil)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if object as AnyObject? === player {
if keyPath == "rate" {
guard let rate = player?.rate else { return }
if rate > Float(0.0) {
VGMediaPlayerCell.playerIsPlaying = true
NotificationCenter.default.post(name: .playerDidStartPlay, object: nil)
} else {
VGMediaPlayerCell.playerIsPlaying = false
NotificationCenter.default.post(name: .playerDidStop, object: nil)
}
}
}
}
func updateUI(with viewModel: VGMediaPlayerViewModel) {
messageLabel.isHidden = true
//indicating waiting state with spinner
waitingStateActive(isActive: viewModel.isLoading)
}
/**
Cancel asset loading
*/
func cancelLoading() {
asset?.cancelLoading()
}
/**
Show an error with a specific message
- parameter message: A message
*/
func displayError(message: String) {
messageLabel.text = message
messageLabel.isHidden = false
containerView.isHidden = true
}
/**
Update the waiting indicator state
- parameter active: A boolean value that indicate if the waiting indicator need to be active or not.
*/
func waitingStateActive(isActive: Bool) {
isActive ? waitingIndicator.startAnimating() : waitingIndicator.stopAnimating()
containerView.isHidden = isActive
}
}
I am in the process of creating a game (Swift) in xcode using a number of SKScene and Sprite objects. I want to create a Scene (settings scene) that captures the player's name, email, gender etc. How can I go about this? How can I capture input from user. SKScenes do not allow input fields/values in the UI?
Thanks
You can build a custom login page that is conform with your game layout without try to rebuild in UIKit the same graphic assets.
Few days ago I've written an answer about SKSceneDelegate to communicate between the scene(SpriteKit) and the viewController (UIKit), take present this answer if you want to call other viewControllers because its the same concept of this answer..
Starting with this GameViewController we can develop some useful methods to handle the login form buttons and show some alerts:
import UIKit
import SpriteKit
class GameViewController: UIViewController, TransitionDelegate {
override func viewDidLoad() {
super.viewDidLoad()
guard let view = self.view as! SKView? else { return }
view.ignoresSiblingOrder = true
view.showsFPS = true
view.showsNodeCount = true
let scene = GameScene(size:view.bounds.size)
scene.scaleMode = .fill
scene.delegate = self as TransitionDelegate
scene.anchorPoint = CGPoint.zero
view.presentScene(scene)
}
func showAlert(title:String,message:String) {
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "Ok", style: .default) { action in
print("handle Ok action...")
})
alertController.addAction(UIAlertAction(title: "Cancel", style: UIAlertActionStyle.cancel, handler: nil))
self.present(alertController, animated: true)
}
func handleLoginBtn(username:String,password:String) {
print("handleLoginBtn")
print("username is: \(username) and password: \(password)")
}
func handleFacebookBtn() {
print("handleFacebookBtn")
}
func handleTwitterBtn() {
print("handleTwitterBtn")
}
}
Then we can make our scene trying to take the advantage of SpriteKit elements:
import SpriteKit
import UIKit
protocol TransitionDelegate: SKSceneDelegate {
func showAlert(title:String,message:String)
func handleLoginBtn(username:String,password:String)
func handleFacebookBtn()
func handleTwitterBtn()
}
class GameScene: SKScene,UITextFieldDelegate {
var usernameTextField:UITextField!
var passwordTextField:UITextField!
var loginBtn:SKShapeNode!
var facebookBtn:SKShapeNode!
var twitterBtn:SKShapeNode!
override func didMove(to view: SKView) {
//bg
let bg = SKSpriteNode(imageNamed: "appleWallpaper")
addChild(bg)
bg.position = CGPoint(x:self.size.width/2,y:self.size.height/2)
//title
let title = SKLabelNode.init(fontNamed: "AppleSDGothicNeo-Bold")
title.text = "xyzGame"; title.fontSize = 25
title.fontColor = .orange
addChild(title)
title.zPosition = 1
title.position = CGPoint(x:self.size.width/2,y:self.size.height-80)
//textfields
guard let view = self.view else { return }
let originX = (view.frame.size.width - view.frame.size.width/1.5)/2
usernameTextField = UITextField(frame: CGRect.init(x: originX, y: view.frame.size.height/4.5, width: view.frame.size.width/1.5, height: 30))
customize(textField: usernameTextField, placeholder: "Enter your username")
view.addSubview(usernameTextField)
usernameTextField.addTarget(self, action:#selector(GameScene.textFieldDidChange(textField:)), for: UIControlEvents.editingChanged)
passwordTextField = UITextField(frame: CGRect.init(x: originX, y: view.frame.size.height/4.5+60, width: view.frame.size.width/1.5, height: 30))
customize(textField: passwordTextField, placeholder: "Enter your password", isSecureTextEntry:true)
view.addSubview(passwordTextField)
//buttons
let myBlue = SKColor(colorLiteralRed: 59/255, green: 89/255, blue: 153/255, alpha: 1)
loginBtn = getButton(frame: CGRect(x:self.size.width/4,y:self.size.height/2,width:self.size.width/2,height:30),fillColor:myBlue,title:"Login",logo:nil,name:"loginBtn")
addChild(loginBtn)
loginBtn.zPosition = 1
let label = SKLabelNode.init(fontNamed: "AppleSDGothicNeo-Regular")
label.text = "or connect with"; label.fontSize = 15
label.fontColor = .gray
addChild(label)
label.zPosition = 1
label.position = CGPoint(x:self.size.width/2,y:self.size.height/2-30)
let logoFb = SKSpriteNode.init(imageNamed: "facebook-icon")
logoFb.setScale(0.5)
facebookBtn = getButton(frame: CGRect(x:self.size.width/4,y:self.size.height/2-80,width:self.size.width/4.5,height:30),fillColor:myBlue,logo:logoFb,name:"facebookBtn")
addChild(facebookBtn)
facebookBtn.zPosition = 1
let myCyan = SKColor(colorLiteralRed: 85/255, green: 172/255, blue: 239/255, alpha: 1)
let logoTw = SKSpriteNode.init(imageNamed: "twitter-icon")
logoTw.setScale(0.5)
twitterBtn = getButton(frame: CGRect(x:self.size.width/2,y:self.size.height/2-80,width:self.size.width/4.5,height:30),fillColor:myCyan,logo:logoTw,name:"twitterBtn")
addChild(twitterBtn)
twitterBtn.zPosition = 1
}
func customize(textField:UITextField, placeholder:String , isSecureTextEntry:Bool = false) {
let paddingView = UIView(frame:CGRect(x:0,y: 0,width: 10,height: 30))
textField.leftView = paddingView
textField.keyboardType = UIKeyboardType.emailAddress
textField.leftViewMode = UITextFieldViewMode.always
textField.attributedPlaceholder = NSAttributedString(string: placeholder,attributes: [NSForegroundColorAttributeName: UIColor.gray])
textField.autocapitalizationType = .none
textField.autocorrectionType = .no
textField.layer.borderColor = UIColor.gray.cgColor
textField.layer.borderWidth = 0.5
textField.layer.cornerRadius = 4.0
textField.textColor = .white
textField.isSecureTextEntry = isSecureTextEntry
textField.delegate = self
}
func getButton(frame:CGRect,fillColor:SKColor,title:String = "",logo:SKSpriteNode!,name:String)->SKShapeNode {
let btn = SKShapeNode(rect: frame, cornerRadius: 10)
btn.fillColor = fillColor
btn.strokeColor = fillColor
if let l = logo {
btn.addChild(l)
l.zPosition = 2
l.position = CGPoint(x:frame.origin.x+(frame.size.width/2),y:frame.origin.y+(frame.size.height/2))
l.name = name
}
if !title.isEmpty {
let label = SKLabelNode.init(fontNamed: "AppleSDGothicNeo-Regular")
label.text = title; label.fontSize = 15
label.fontColor = .white
btn.addChild(label)
label.zPosition = 3
label.position = CGPoint(x:frame.origin.x+(frame.size.width/2),y:frame.origin.y+(frame.size.height/4))
label.name = name
}
btn.name = name
return btn
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
let positionInScene = touch!.location(in: self)
let touchedNode = self.atPoint(positionInScene)
if let name = touchedNode.name {
switch name {
case "loginBtn":
self.run(SKAction.wait(forDuration: 0.1),completion:{[unowned self] in
guard let delegate = self.delegate else { return }
(delegate as! TransitionDelegate).handleLoginBtn(username:self.usernameTextField.text!,password: self.passwordTextField.text!)
})
case "facebookBtn":
self.run(SKAction.wait(forDuration: 0.1),completion:{[unowned self] in
guard let delegate = self.delegate else { return }
(delegate as! TransitionDelegate).handleFacebookBtn()
})
case "twitterBtn":
self.run(SKAction.wait(forDuration: 0.1),completion:{[unowned self] in
guard let delegate = self.delegate else { return }
(delegate as! TransitionDelegate).handleTwitterBtn()
})
default:break
}
}
}
func textFieldDidChange(textField: UITextField) {
//print("everytime you type something this is fired..")
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
return true
}
func textFieldDidEndEditing(_ textField: UITextField) {
if textField == usernameTextField { // validate email syntax
let emailRegEx = "[A-Z0-9a-z._%+-]+#[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"
let emailTest = NSPredicate(format:"SELF MATCHES %#", emailRegEx)
let result = emailTest.evaluate(with: textField.text)
let title = "Alert title"
let message = result ? "This is a correct email" : "Wrong email syntax"
if !result {
self.run(SKAction.wait(forDuration: 0.01),completion:{[unowned self] in
guard let delegate = self.delegate else { return }
(delegate as! TransitionDelegate).showAlert(title:title,message: message)
})
}
}
}
deinit {
print("\n THE SCENE \((type(of: self))) WAS REMOVED FROM MEMORY (DEINIT) \n")
}
}
Output:
Animated output:
As you can see we can handle both framework with their delegate methods, I've tested this page with iPhone 5 and iPhone 7 plus.