I'm building audio book app.
I have play button inside my collection view cell where users see list of all mp3 files. When user presses this button (play button) - I'm sending data of track to my player class and music file starts to play. I'm using global variables to send track details to my player.
Here's my code:
extension ChapterDetailsViewController: AliaCellDelegate {
func playAlia(cell: AliaCell) {
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "playAudio"), object: nil)
}
}
I need to implement such functionality: When track comes to an end - automatically play next file from same list. For this, I have this method inside my player class, that informs me, when it has finished playing:
func playerDidFinishPlaying(note: NSNotification) {
print("Finished playing")
}
But I don't know how to call func playAlia(cell: AliaCell) inside func playerDidFinishPlaying(note: NSNotification) in my player class and pass details of the next track from my collection view.
For now I have created only:
NotificationCenter.default.addObserver(self, selector: #selector(playNext), name: NSNotification.Name(rawValue: "playNext"), object: nil) but what should I do next?
A simple solution is to keep track of existing track within your source model. The example makes plenty of assumptions and is not it not complete.
struct Track {
let url: URL
let title: String
let artist: String
}
class AudioPlayer: UIViewController {
var currentTrackIndex: Int = 0
let tracks: [Track] = [Track(), Track(), Track()]
let player: AVPlayer()
// table view delegate for row selection
override func tableView(_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath) {
// assuming single section
currentTrackIndex = indexPath.row
playTrack(at: currentTrackIndex)
}
func playTrack(at index: Int) {
let nextTrack = tracks[index]
let nextItem = AVPlayerItem(url: nextTrack.url)
player.replaceCurrentItem(with: nextItem)
}
func playerDidFinishPlaying(note: NSNotification) {
// add logic to handle out of bounds index for array
currentTrackIndex += 1
playTrack(at: currentTrackIndex)
}
}
Related
I am writing several Swift multiplayer games based on the Ray Wenderlich tutorial for Nine Knights. (https://www.raywenderlich.com/7544-game-center-for-ios-building-a-turn-based-game)
I use pretty much the same GameCenterHelper file except that I change to a segue instead of present scene since I am using UIKit instead of Sprite Kit with the following important pieces:
present match maker:
func presentMatchmaker() {
guard GKLocalPlayer.local.isAuthenticated else {return}
let request = GKMatchRequest()
request.minPlayers = 2
request.maxPlayers = 2
request.inviteMessage = "Would you like to play?"
let vc = GKTurnBasedMatchmakerViewController(matchRequest: request)
vc.turnBasedMatchmakerDelegate = self
currentMatchmakerVC = vc
print(vc)
viewController?.present(vc, animated: true)
}
the player listener function:
extension GameCenterHelper: GKLocalPlayerListener {
func player(_ player: GKPlayer, receivedTurnEventFor match: GKTurnBasedMatch, didBecomeActive: Bool) {
if let vc = currentMatchmakerVC {
currentMatchmakerVC = nil
vc.dismiss(animated: true)
}
guard didBecomeActive else {return}
NotificationCenter.default.post(name: .presentGame, object: match)
}
}
The following extension for Notification Center:
extension Notification.Name {
static let presentGame = Notification.Name(rawValue: "presentGame")
static let authenticationChanged = Notification.Name(rawValue: "authenticationChanged")
}
In the viewdidload of the menu I call the following:
override func viewDidLoad() {
super.viewDidLoad()
createTitleLabel()
createGameImage()
createButtons()
GameCenterHelper.helper.viewController = self
GameCenterHelper.helper.currentMatch = nil
NotificationCenter.default.addObserver(self, selector: #selector(authenticationChanged(_:)), name: .authenticationChanged, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(presentGame(_:)), name: .presentGame, object: nil)
}
and tapping the multi device buttons calls the following:
#objc func startMultiDeviceGame() {
multiPlayer = true
GameCenterHelper.helper.presentMatchmaker()
}
and the notifications call the following:
#objc func presentGame(_ notification: Notification) {
// 1
print("present game")
guard let match = notification.object as? GKTurnBasedMatch else {return}
loadAndDisplay(match: match)
}
// MARK: - Helpers
private func loadAndDisplay(match: GKTurnBasedMatch) {
match.loadMatchData { [self] data, error in
if let data = data {
do {
gameModel = try JSONDecoder().decode(GameModel.self, from: data)
} catch {gameModel = GameModel()}
} else {gameModel = GameModel()}
GameCenterHelper.helper.currentMatch = match
print("load and display")
performSegue(withIdentifier: "gameSegue", sender: nil)
}
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
print("prepare to segue")
if let vc = segue.destination as? GameVC {vc.gameModel = gameModel}
}
Which is hopefully easy to follow.
The game starts and the menu scene adds the observer for present game
The player taps multi device, which presents the matchmaker
The player selects their game from the match maker, which I think activates the player listener function
This posts to the Notification Center for present game
The notification center observer calls present game, which calls load and display, with a little help from prepare segue
My issue is that the first time I do this it works perfectly, and per the framework from that tutorial that I can't figure out how to change (an issue for a different question I think) after a player takes their turn they are returned to the menu. The second time they enter present matchmaker and select a game the present game function is called twice, and the third time they take their turn without shutting down the app it is called 3 times, etc. (I have the print statements in both the present game and load and display functions and they are called back to back the 2nd time through and back to back to back the 3rd time etc. even though they are only called once the first time a game is selected from the matchmaker)
Console messages
present matchmaker true
<GKTurnBasedMatchmakerViewController: 0x104810000>
present game
present game
present game
load and display
prepare to segue
load and display
prepare to segue
load and display
prepare to segue
2021-03-20 22:32:26.838680-0600 STAX[4997:435032] [Presentation] Attempt to present <STAX.GameVC: 0x103894c00> on <Game.MenuVC: 0x103814800> (from < Game.MenuVC: 0x103814800>) whose view is not in the window hierarchy.
(419.60100000000006, 39.0)
2021-03-20 22:32:26.877943-0600 STAX[4997:435032] [Presentation] Attempt to present <STAX.GameVC: 0x103898e00> on < Game.MenuVC: 0x10501c800> (from < Game.MenuVC: 0x10501c800>) whose view is not in the window hierarchy.
I had thought that this was due to me not removing the Notification Center observers, but I tried the following in the view did load for the menu screen (right before I added the .presentGame observer):
NotificationCenter.default.removeObserver(self, name: .presentGame, object: nil)
and that didn't fix the issue, so I tried the following (in place of the above):
NotificationCenter.default.removeObserver(self)
and that didn't work so I tried them each, one at a time in the view did disappear of the game view controller (which I didn't think would work since self refers to the menu vc, but I was getting desperate) and that didn't work either.
I started thinking that maybe I'm not adding multiple observers that are calling present game more than once, since the following didn't work at all the second time (I'm just using a global variable to keep track of the first run through that adds the observers and then not adding them the second time):
if addObservers {
NotificationCenter.default.addObserver(self, selector: #selector(authenticationChanged(_:)), name: .authenticationChanged, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(presentGame(_:)), name: .presentGame, object: nil)
addObservers = false
}
since it is trying to add a view that is not in the view hierarchy. (Although the background music for that screen starts playing, the menu remains and the game board is not shown...)
I wasn't sure if I'm removing the Notification Center observers incorrectly or if they aren't really the source of the problem so I decided to ask for help :)
Thank you!
I figured it out. I was trying to remove the Notifications from a deallocated instance of the view controller per the below link (The bottom most answer):
How to avoid adding multiple NSNotification observer?
The correct way to remove the notifications is in the view will disappear function like this:
override func viewWillDisappear(_ animated: Bool) {
NotificationCenter.default.removeObserver(self, name: Notification.Name.presentGame, object: nil)
NotificationCenter.default.removeObserver(self, name: Notification.Name.authenticationChanged, object: nil)
}
After implementing that I stopped making multiple calls to the notification center.
Is there a notification mechanism for tooltips, so then key modifiers can be used to make them dynamic? I do not know of one so went about this path to capture the tooltip view being created and trying to trigger a redraw when a key event occurs.
I monitor key modifier(s) (in my app delegate); focusing on SHIFT here but any key modifier will do:
var localKeyDownMonitor : Any? = nil
var globalKeyDownMonitor : Any? = nil
var shiftKeyDown : Bool = false {
didSet {
let notif = Notification(name: Notification.Name(rawValue: "shiftKeyDown"),
object: NSNumber(booleanLiteral: shiftKeyDown));
NotificationCenter.default.post(notif)
}
}
which the app's startup process will install ...
// Local/Global Monitor
_ /*accessEnabled*/ = AXIsProcessTrustedWithOptions([kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true] as CFDictionary)
globalKeyDownMonitor = NSEvent.addGlobalMonitorForEvents(matching: NSEventMask.flagsChanged) { (event) -> Void in
_ = self.keyDownMonitor(event: event)
}
localKeyDownMonitor = NSEvent.addLocalMonitorForEvents(matching: NSEventMask.flagsChanged) { (event) -> NSEvent? in
return self.keyDownMonitor(event: event) ? nil : event
}
and then intercept to post notifications via app delegate didSet above (so when the value changes a notification is sent!):
func keyDownMonitor(event: NSEvent) -> Bool {
switch event.modifierFlags.intersection(.deviceIndependentFlagsMask) {
case [.shift]:
self.shiftKeyDown = true
return true
default:
// Only clear when true
if shiftKeyDown { self.shiftKeyDown = false }
return false
}
}
For singleton tooltip bindings, this can be dynamic by having the notification inform (KVO of some key) I'd like a view delegate routine to act on this:
func tableView(_ tableView: NSTableView, toolTipFor cell: NSCell, rect: NSRectPointer, tableColumn: NSTableColumn?, row: Int, mouseLocation: NSPoint) -> String {
if tableView == playlistTableView
{
let play = (playlistArrayController.arrangedObjects as! [PlayList])[row]
if shiftKeyDown {
return String(format: "%ld play(s)", play.plays)
}
else
{
return String(format: "%ld item(s)", play.list.count)
}
}
...
So I'd like my notification handler to do something like
internal func shiftKeyDown(_ note: Notification) {
let keyPaths = ["cornerImage","cornerTooltip","<table-view>.<column-view>"]
for keyPath in (keyPaths)
{
self.willChangeValue(forKey: keyPath)
}
if subview.className == "NSCustomToolTipDrawView" {
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "CustomToolTipView"), object: subview)
}
for keyPath in (keyPaths)
{
self.didChangeValue(forKey: keyPath)
}
}
where I would like to force the tooltip to redraw, but nothing happens.
What I did to obtain the tooltip view, was to watch any notification (use nil name to do this) that looked promising and found one - by monitoring all notifications and so I post to it for interested view controller which is the delegate of the tableView. The view controller is observing for the "CustomToolTipView" and remembers the object send - tooltipView; this is the new tooltip view created.
But nothing happens - shiftKeyDown(), to redraw the view.
I suspect it won't update in the current event loop but I do see the debug output.
With Willeke's suggestion I migrated a tableView to be cell based, added a get property for the tooltip and had the relevant object's init() routine register notifications for the shift key changes. This resulted in less code. A win-win :-)
I have a download function that downloads files from a website. Everything works great except for when I click on the back button from the navigation controller and try to reload the view controller. The download task runs fine in the background but resets the view on the second reload. Here's my code.
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask,
didWriteData bytesWritten: Int64, totalBytesWritten: Int64,
totalBytesExpectedToWrite: Int64) {
// 1
guard let url = downloadTask.originalRequest?.url,
let download = downloadService.activeDownloads[url] else { return }
// 2
download.progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
// 3
let totalSize = ByteCountFormatter.string(fromByteCount: totalBytesExpectedToWrite,
countStyle: .file)
// 4
DispatchQueue.main.async {
if let myCell = self.tableView.cellForRow(at: IndexPath(row: Int(download.resource.resourceId - 1) ,
section: 0)) as? TranslationViewCell {
myCell.updateDisplay(progress: download.progress, totalSize: totalSize)
if download.isDownloading == true{
myCell.downloadButton.isHidden = true //this doesnt get activated at all.
myCell.reloadInputViews()
}
}
}
}
I am trying to keep the download button hidden if the download is running in the background.
When a view controller gets popped off the stack it gets deallocated. This means that the 'self' you reference in the code no longer exists, which makes it impossible to update the view.
In order to load up the view correctly the second time, you need to be holding onto the download task in code that does not get deallocated or use something like events to respond to the download task itself.
We could devolve into an entire discussion of iOS app architecture at this point; however I'll give you my favorite option to just get this working.
Send a Notification rather than interacting with the tableview directly. The code might look something like this...
First extend the notification class for your own app:
extension Notification.Name {
static let downloadProgressChanged = Notification.Name("download_progress_changed")
}
Then post the notification during the callback:
DispatchQueue.main.async {
let notificationDict:[String: Download] = ["download": download]
NotificationCenter.default.post(name: .downloadProgressChanged, object: nil, userInfo: userDict)
}
Then your view controller will need to listen for those notifications and update themselves appropriately, you should probably call this in viewDidLoad
// MARK - Notifications
func setupListeners() {
NotificationCenter.default.addObserver(self, selector: #selector(handleProgressChanged(notification:)), name: .downloadProgressChanged, object: nil)
}
#objc func handleProgressChanged(notification: NSNotification) {
if let download = notification.userInfo?["download"] as? <whatever the class of Download is> {
// update the tableview here
}
}
Now you have a view controller that can respond to this download task appropriately without needing to know anything about the task at all.
I am new to OS X app development. I manage to built the NSComboBox (Selectable, not editable), I can get it indexOfSelectedItem on action button click, working fine.
How to detect the the value on change? When user change their selection, what kind of function I shall use to detect the new selected index?
I tried to use the NSNotification but it didn't pass the new change value, always is the default value when load. It is because I place the postNotificationName in wrong place or there are other method should use to get the value on change?
I tried searching the net, video, tutorial but mostly written for Objective-C. I can't find any answer for this in SWIFT.
import Cocoa
class NewProjectSetup: NSViewController {
let comboxRouterValue: [String] = ["No","Yes"]
#IBOutlet weak var projNewRouter: NSComboBox!
#IBAction func btnAddNewProject(sender: AnyObject) {
let comBoxID = projNewRouter.indexOfSelectedItem
print(“Combo Box ID is: \(comBoxID)”)
}
#IBAction func btnCancel(sender: AnyObject) {
self.dismissViewController(self)
}
override func viewDidLoad() {
super.viewDidLoad()
addComboxValue(comboxRouterValue,myObj:projNewRouter)
self.projNewRouter.selectItemAtIndex(0)
let notificationCenter = NSNotificationCenter.defaultCenter()
notificationCenter.addObserver(
self,
selector: “testNotication:”,
name:"NotificationIdentifier",
object: nil)
NSNotificationCenter.defaultCenter().postNotificationName("NotificationIdentifier", object: projNewRouter.indexOfSelectedItem)
}
func testNotication(notification: NSNotification){
print("Found Combo ID \(notification.object)")
}
func addComboxValue(myVal:[String],myObj:AnyObject){
let myValno: Int = myVal.count
for var i = 0; i < myValno; ++i{
myObj.addItemWithObjectValue(myVal[i])
}
}
}
You need to define a delegate for the combobox that implements the NSComboBoxDelegate protocol, and then use the comboBoxSelectionDidChange(_:) method.
The easiest method is for your NewProjectSetup class to implement the delegate, as in:
class NewProjectSetup: NSViewController, NSComboBoxDelegate { ... etc
Then in viewDidLoad, also include:
self.projNewRouter.delegate = self
// self (ie. NewProjectSetup) implements NSComboBoxDelegate
And then you can pick up the change in:
func comboBoxSelectionDidChange(notification: NSNotification) {
print("Woohoo, it changed")
}
I'm trying to programatically get get a a column.identifier for the cell that is being edited. I'm trying to get by registering my NSViewController for NSControlTextDidBeginEditingNotification and when I get the notification I track the data by mouse location:
var selectedRow = -1
var selectedColumn: NSTableColumn?
func editingStarted(notification: NSNotification) {
selectedRow = participantTable.rowAtPoint(participantTable.convertPoint(NSEvent.mouseLocation(), fromView: nil))
let columnIndex = participantTable.columnAtPoint(participantTable.convertPoint(NSEvent.mouseLocation(), fromView: nil))
selectedColumn = participantTable.tableColumns[columnIndex]
}
The problem I have is that the mouse location is giving me the wrong data, is there a way to get the mouse location based on the location of the table, or could there be a better way to get this information?
PS. My NSViewController is NSTableViewDelegate and NSTableViewDataSource, my NSTableView is View Based and connects to an ArrayController which updates correctly, and I could go to my Model object and detect changes in the willSet or didSet properties, but I need to detect when a change is being made by the user and this is why I need to detect the change before it happens on the NSTableView.
This question is 1 year old but I got the same issue today and fixed it. People helped me a lot here so I will contribute myself if someone found this thread.
Here is the solution :
1/ Add the NSTextFieldDelegate to your ViewController :
class ViewController: NSViewController, NSTableViewDelegate, NSTableViewDataSource, NSTextFieldDelegate {
2/ When a user wants to edit a cell, he had first to select the row. So we will detect that with this delegate function :
func tableViewSelectionDidChange(_ notification: Notification) {
let selectedRow = self.tableView.selectedRow
// If the user selected a row. (When no row is selected, the index is -1)
if (selectedRow > -1) {
let myCell = self.tableView.view(atColumn: self.tableView.column(withIdentifier: "myColumnIdentifier"), row: selectedRow, makeIfNecessary: true) as! NSTableCellView
// Get the textField to detect and add it the delegate
let textField = myCell.textField
textField?.delegate = self
}
}
3/ When the user will edit the cell, we can get the event (and the data) with 3 different functions. Pick the ones you need :
override func controlTextDidBeginEditing(_ obj: Notification) {
// Get the data when the user begin to write
}
override func controlTextDidEndEditing(_ obj: Notification) {
// Get the data when the user stopped to write
}
override func controlTextDidChange(_ obj: Notification) {
// Get the data every time the user writes a character
}