I'm building an app that makes use of MPMusicPlayerApplicationController to play music from a user's local music library.
The songs from the library are retrieved like so:
func retrieveMusic() {
mediaQueryQueue.async {
let songsQuery = MPMediaQuery.songs()
let songsCollections = songsQuery.collections
for songCollection in songsCollections ?? [] {
let songs = songCollection.items
self.retrievedSongs.append(contentsOf: songs)
}
DispatchQueue.main.async {
self.setLoadedSongsUIState()
self.setupTableView()
self.sortOrderDidChange()
}
}
}
These songs are then displayed in a table view:
func setTableViewItems(_ songs: [MPMediaItem]) {
let section = self.tableViewAdaptor!.sections[0] as! TableViewAdaptorSection<SongTableViewCell, MPMediaItem>
section.items = songs
tableViewAdaptor.tableView.reloadData()
}
I also have a segment control that will determine whether to display the songs in the order in which they were retrieved, or in the last played order; and then update the table view based on which choice was selected:
var retrievedSongs: [MPMediaItem] = []
var sortedSongs: [MPMediaItem] = []
private func sortOrderDidChange() {
switch sortOrder {
case .playDateDescending:
let unixDate = Date(timeIntervalSince1970: 1000)
sortedSongs = retrievedSongs.sorted {
$0.lastPlayedDate ?? unixDate > $1.lastPlayedDate ?? unixDate
}
default:
sortedSongs = retrievedSongs
}
setTableViewItems(sortedSongs)
let queueDescriptor = MPMusicPlayerMediaItemQueueDescriptor(itemCollection: MPMediaItemCollection(items: sortedSongs))
queueDescriptor.startItem = musicPlayer.nowPlayingItem
musicPlayer.setQueue(with: queueDescriptor)
selectPlayingItem(scrollToVisible: true)
}
The user is able to use buttons to play/pause, and go to the next or previous tracks:
#IBAction func forwardButtonPressed(_ sender: Any) {
musicPlayer.skipToNextItem()
selectPlayingItem(scrollToVisible: true)
}
#IBAction func backButtonPressed(_ sender: Any) {
musicPlayer.skipToPreviousItem()
selectPlayingItem(scrollToVisible: true)
}
Additionally, if you select a track in the table view, that track will be played:
tableViewAdaptor = TableViewAdaptor(
tableView: songTableView,
sections: [songsLibrarySection],
didChangeHandler: {
let selectedSongIndex = self.songTableView.indexPathForSelectedRow
if selectedSongIndex != nil {
let selectedSong = self.retrievedSongs[selectedSongIndex!.row]
self.musicPlayer.nowPlayingItem = selectedSong
self.musicPlayer.play()
self.updatePlaybackUI(scrollNowPlayingToVisible: true)
}
})
setTableViewItems(retrievedSongs)
When the songs are displayed in the order in which they were retrieved, everything works perfectly. If I click back or forwards, or select a song to play; the correct song will play in the next order.
Right now if you're in the "order retrieved" tab, and you go to the "ordered by date" tab and click backwards or forward, the next song will not be whatever is next that's displayed in the table view, but in the "retrieved" by order.
What I'm trying to do is change the code so that no matter whether the retrieved or ordered songs are displayed, the next or previous song will correspond to what's displayed.
Related
I made a menu where the user can access multiple buttons to rotate and change the status of a document. Every time the user selects a button the changes will be written into the realm database.
//changes the status of the dc
case "changeStatus" :
if let marker = documentationMarkers.first(where: {$0.documentationId == doc.id}) {
let nav = UINavigationController()
let ctrl = StatusPickerViewController()
ctrl.delegate = self
nav.pushViewController(ctrl, animated: true)
nav.modalPresentationStyle = .popover
self.present(nav, animated: true)
nav.popoverPresentationController?.permittedArrowDirections = .any
nav.popoverPresentationController?.sourceView = marker
nav.popoverPresentationController?.sourceRect = marker.bounds
}
//rotates the document
case "rotateMarker" :
if let marker = documentationMarkers.first(where: {$0.documentationId == doc.id}) {
marker.rotate()
let documentation = documentations.first(where: {$0.id == marker.documentationId})
var rotation = documentation?.rotation.value ?? 0
rotation = (rotation + 15) % 360
_ = self.updateRotation(documentation: documentation!, rotationAngle: rotation)
}
updateRotation method is in a storage extension and writes the changes into the db:
func updateRotation(documentation: Documentation, rotationAngle: Int) throws {
if let realm = try? Realm() {
try realm.write {
documentation.updated = Date()
documentation.rotation.value = rotationAngle
}
return
}
throw MobiplanError.invalidStateError(message: "Opening the database failed")
}
My confirm button is empty at the moment cause I did not know how to implement it so that the changes will be committed into the database when its pressed and not every time another button is selected.
So f.e. when you press the rotate button 100 times, it changes the database 100 times. It should only change it once when you press confirm.
I thought of a struct with nullable types with the rotation and status
struct DocumentationChanges {
var statusId: String
var rotation: Int
}
Keep in mind that your current code doesn't write anything to your Realm database, nor your struct has any nullable type.
In order to implement what you want, you could create a DocumentationChanges array, and everytime your user press the rotation button, save the new rotation in the array.
When the user clicks the confirm button, save the array in your Realm database.
Something like this to save your rotations:
var rotationStory = [DocumentationChanges]()
func updateRotation(documentation: Documentation, rotationAngle: Int) {
let documentationChange = DocumentationChanges(statusId, rotationAngle)
rotationStory.append(documentationChange)
}
And then this to store all the performed rotations in the database, once the confirm button is clicked:
func storeToRealm() {
do {
if let database = try? Realm()
try database.write {
database.add(rotationStory)
}
} catch {
throw
}
}
Hello I am trying to assign an array of Strings to display in a scrollView based off of where a user is located.
I am working on an iOS app that sometimes displays info about THC(Weed). Based on certain states in the United States I can not display certain things about THC.
To my knowledge I know this is the only way I can get the users Country and State/Administrative area.
func storeStateAndCountry() {
let locationManager = CLLocationManager()
CLGeocoder().reverseGeocodeLocation(locationManager.location!) { (placemark, error) in
if error != nil {
print("error")
} else {
let place = placemark! as [CLPlacemark]
if place.count > 0 {
let place = placemark![0]
let display = self.displayTHC(country: place.country! , adminArea: place.administrativeArea!)
let phone = Phone(context: PersistenceServce.context)
phone.canDisplayTHC = display
PersistenceServce.saveContext()
}
}
}
}
Below I call store storeStateAndCounty() on a login screen but, I believe the closure is not letting me finish this but, I am not sure how to get around it.
override func viewDidLoad() {
super.viewDidLoad()
self.storeStateAndCountry()
self.navigationController?.navigationBar.isHidden = true
self.hideKeyboardWhenTappedAround()
}
Below you can see I am trying to store it in Core Data and then take the data back out in viewDidLoad. But nothing is being stored. This HomeVC is where the app crashes.
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.interactivePopGestureRecognizer?.isEnabled = false
self.homeTableView.estimatedRowHeight = 50
let fetchRequest: NSFetchRequest<Phone> = Phone.fetchRequest()
do {
print("Starting a fetch")
let phone = try PersistenceServce.context.fetch(fetchRequest)
self.phone = phone
if self.phone[0].canDisplayTHC == true{//runtime error invalid index
self.titleArray = ["THC should display here", "THC should display here", "THC should display here", "THC should display here"]
}else{
self.titleArray = ["Is Microdosing Cannabis Really Effective?", "Another article with a different title","San Francisco hires Head of Cannabis","Another article with a different title"]
}
}catch{}
}
Any Ideas?
Okay so I have two view controllers. One view controller loads up all the cells that is on my Plist and the second view controller opens up the cell and shows you the description. For example:
View Controller 1:
Dog
Cat
Mouse
Click on Dog cell it will take you to View Controller 2:
Dog goes Woof.
view controller 1 is written:
ovverride func prepare(for segue: UIStoryBoardSegue, sender: Any?) {
if segue.identifier == "showDetail" {
let animals: Animals
if isFiltering() {
animals = filteredData[indexPath.row]
}
else {
animals = originalData[indexPath.row]
}
let controller = (segue.destination as! UINavigationController).topViewController as! SecondViewController
controller.detailedAnimals = animals
controller.navigationItem.leftBarButtonItem = splitViewController?.displayModeButtonItem
contrller.navigationItem.leftItemsSupplementBackButton = true
}
}
}
this is what i wrote in viewcontroller 2 updated
var isFavorite : Bool = false
#IBAction func addToFav(_ sender:UIButton) {
isFavorite = !isFavorite
UpdateButtonAppearance()
saveData()
}
private func UpdateButtonAppearance(){
if isFavorite{
let image = UIImage(named: "addFav")
favButton.setImage(image, for: . normal)
savedData()
}
else {
let image = UIImage(named: "addFavFilled")
favButton.setImage(image, for: . normal)
savedData()
}
}
ovveride func viewDidLoad(){
UpdateButtonAppearance()
saveData()
}
//updated code
func getFilePath () -> String {
var path: [AnyObject] = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true) as [AnyObject]
let documentsDirectory: String = path[0] as! String
let filepath = documentsDirectory.appending("Animals.plist")
return filepath
}
func saveData(){
let myDict : NSMutableDictionary = NSMutableDictionary()
myDict["fav"] = NSNumber(booleanLiteral: isFavorite)
myDict.write(toFile: self.getFilePath(), atomically: true)
}
func getBool(){
if FileManager.default.fileExists(atPath: self.getFilePath()) {
var myDict = NSDictionary(contentsOfFile: self.getFilePath()) as!
[String:AnyObject]
let myBool: Bool = myDict["fav"]!.boolValue
isFavorite = myBool
}
I saw a tutorial on how to change the bool in the Plist and wrote it this way. The code compiles but I don't think it is changing the bool value. So on my Animals Plist i have a Item 0 type dictionary, first key is called Animal, type is string, value is "dog" and second key is Description, type is string, value is "dog goes woof" and third key is called fav, type is Bool and value is No for now but I am trying to change this value to Yes but it is not working. Also thank you so much for your comment it was easy to follow and easy to understand.
The star is not filled when you go back to the 2nd view controller because you set the images in the #IBAction func addToFav(_ sender:UIButton) method. That is when you tap the button.
You should have that part of the code in another method that you also call in didLoad().
2nd View Controller should be something like this:
var isFavorite = UserDefaults.standard.bool(forKey: "isFavorite")
func didLoad() {
super.didLoad()
updateButtonAppearance()
}
private updateButtonAppearance() {
if isFavorite {
let image = UIImage(named: "addFavFilled")
button.setImage(image, for: . normal)
}
else {
let image = UIImage(named: "addFav")
button.setImage(image, for: . normal)
}
}
#IBAction func addToFav(_ sender:UIButton) {
isFavorite = !isFavorite
UserDefaults.standard.set(isFavorite, forKey: "isFavorite")
updateButtonAppearance()
}
Also the code could be improved to not save the variable in UserDefaults in addToFav method, but whenever isFavourite is changed. Maybe you will later want to change the state of isFavourite in another method which will require code duplication.
Also note that you save a value for all pets in UserDefaults under isFavourite. That means if you favor one pet, all other pets will be favored and vice versa. Consider replacing the bool value in user defaults with a dictionary that has keys for each pet and booleans as values.
So I'm working on a quiz-app and I want to give the user an option screen before starting the quiz, where the user can choose which categories the quiz should have.
Its currently only working with 1 category at a time but I want to be able to concatenate the questionArrays when the user selects button in the option screen.
At this time the user selects a button and depending on the sender.tag an Int gets passed through the var called 'pickedCategory' which then decides which of the arrays to base the questions on. This happens in the prepare for segue:
let selectedCategory = quizCategories[pickedCategory]
secondVC.allQuestions = selectedCategory.questions
quizCategories is declared at the top:
var quizCategories = [QuestionArray]()
What I would like to do is have 4 buttons and whenever one is selected, concatenate an array to the selectedCategory
For instance, button 1 & 2 is selected, i.e. the user wants to have questions based on 2 categories. Resulting in something like this?
let selectedCategory = quizCategories[0].questions + quizCategories[1].questions
and if 3 buttons selected, add another quizCategories array to the final array etc etc
Please note, its not HOW to concatenate I'm looking for, its how to automatically do this depending on buttons selected..
This is my first question posted and I am very new to Swift.. hope I didn't confuse u guys.. thanks :)
What I would suggest is for each category button, keep track of which is selected (and even deselected if the user decides they don't want that category), by adding or removing from an array of categoryIDs. Then, once the "Done" button (or whatever final decision button is tapped), take the array of categoryIDs they selected, and flatMap over them to produce the ultimate array of questions.
let questions = categoryIDs.flatMap { quizCategories[$0].questions }
Now, you'd have your array of questions to present to the user. Use flatMap vs map, because you'd want to flatten the returned array of arrays into a single array.
Tags are not the best way to identify buttons, but I'll ignore that.
Don't use buttons, use UISwitches. Have a switch for each category.
Say you have a constant switchTagBase, with a value of 100:
let switchTagBase = 100
So the switch for your first category has a tag of 100, the next one has a tag of 101, etc.
You already have an array of categories, which is good.
Now, when it's time to build your array of selected questions, simply go through the categories, figure out which switches are on, and add in those categories
var selectedCategories = [QuestionArray]()
for (index, array) in quizCategories.enumerated() {
let thisTag = index + switchTagBase
guard let thisSwitch = view.viewWithTag(thisTag),
thisSwitch.isOn else { continue }
selectedCategories += quizCategories[index]
}
(Note that you should really maintain an array of selection states based on the switches the user activates, and iterate through that array rather than looping through the switches directly, but in the interest of brevity I fetched switch states directly.)
You can track which button is selected and when you want to start quizz, concatenate the corresponding arrays.
Your code could look like this:
import UIKit
class QuizzSelection: UIViewController
{
#IBAction func firstButtonAction(_ sender: Any)
{
firstButtonWasClicked()
}
#IBAction func secondButtonAction(_ sender: Any)
{
secondButtonWasClicked()
}
#IBAction func goForwardWithQuizz(_ sender: Any)
{
startQuizzWasClicked()
}
var isFirstCategorySelected = false
var isSecondCategorySelected = false
let firstCategoryQuestions = ["Question Cat 1"]
let secondCategoryQuestions = ["Question Cat 2"]
var emptyArrayOfQuestions = [String]()
func firstButtonWasClicked()
{
if isFirstCategorySelected == true{
isFirstCategorySelected = false
} else{
isFirstCategorySelected = true
}
}
func secondButtonWasClicked()
{
if isSecondCategorySelected == true{
isSecondCategorySelected = false
} else{
isSecondCategorySelected = true
}
}
func startQuizzWasClicked()
{
if isFirstCategorySelected == true{
emptyArrayOfQuestions += firstCategoryQuestions
}
if isSecondCategorySelected == true{
emptyArrayOfQuestions += secondCategoryQuestions
}
}
}
EDIT
Improved code for six categories:
import UIKit
class QuizzSelection: UIViewController
{
#IBAction func firstButtonAction(_ sender: Any)
{
firstButtonWasClicked()
}
#IBAction func secondButtonAction(_ sender: Any)
{
secondButtonWasClicked()
}
// Four mour button actions
#IBAction func goForwardWithQuizz(_ sender: Any)
{
startQuizzWasClicked()
}
var wichCategoryAreSelected = [false, false, false, false, false, false] //six categories
var arrayOfQuestions = [["Question 1 Cat 1","Question 2 Cat 1"], ["Question 1 Cat 2", "Question 2 Cat 2"], ...]
var emptyArrayOfQuestions = [String]()
func firstButtonWasClicked()
{
wichCategoryAreSelected[0] = !wichCategoryAreSelected[0]
}
func secondButtonWasClicked()
{
wichCategoryAreSelected[1] = !wichCategoryAreSelected[1]
}
func startQuizzWasClicked()
{
for i in 0...(wichCategoryAreSelected.count-1)
{
if wichCategoryAreSelected[i]
{
emptyArrayOfQuestions += arrayOfQuestions[i]
}
}
}
}
I have this Firebase data:
I want to query the posts data through pagination. Currently my code is converting this JS code to Swift code
let postsRef = self.rootDatabaseReference.child("development/posts")
postsRef.queryOrderedByChild("createdAt").queryStartingAtValue((page - 1) * count).queryLimitedToFirst(UInt(count)).observeSingleEventOfType(.Value, withBlock: { snapshot in
....
})
When accessing, this data page: 1, count: 1. I can get the data for "posts.a" but when I try to access page: 2, count: 1 the returns is still "posts.a"
What am I missing here?
Assuming that you are or will be using childByAutoId() when pushing data to Firebase, you can use queryOrderedByKey() to order your data chronologically. Doc here.
The unique key is based on a timestamp, so list items will automatically be ordered chronologically.
To start on a specific key, you will have to append your query with queryStartingAtValue(_:).
Sample usage:
var count = numberOfItemsPerPage
var query ref.queryOrderedByKey()
if startKey != nil {
query = query.queryStartingAtValue(startKey)
count += 1
}
query.queryLimitedToFirst(UInt(count)).observeSingleEventOfType(.Value, withBlock: { snapshot in
guard var children = snapshot.children.allObjects as? [FIRDataSnapshot] else {
// Handle error
return
}
if startKey != nil && !children.isEmpty {
children.removeFirst()
}
// Do something with children
})
I know I'm a bit late and there's a nice answer by timominous, but I'd like to share the way I've solved this. This is a full example, it isn't only about pagination. This example is in Swift 4 and I've used a nice library named CodableFirebase (you can find it here) to decode the Firebase snapshot values.
Besides those things, remember to use childByAutoId when creating a post and storing that key in postId(or your variable). So, we can use it later on.
Now, the model looks like so...
class FeedsModel: Decodable {
var postId: String!
var authorId: String! //The author of the post
var timestamp: Double = 0.0 //We'll use it sort the posts.
//And other properties like 'likesCount', 'postDescription'...
}
We're going to get the posts in the recent first fashion using this function
class func getFeedsWith(lastKey: String?, completion: #escaping ((Bool, [FeedsModel]?) -> Void)) {
let feedsReference = Database.database().reference().child("YOUR FEEDS' NODE")
let query = (lastKey != nil) ? feedsReference.queryOrderedByKey().queryLimited(toLast: "YOUR NUMBER OF FEEDS PER PAGE" + 1).queryEnding(atValue: lastKey): feedsReference.queryOrderedByKey().queryLimited(toLast: "YOUR NUMBER OF FEEDS PER PAGE")
//Last key would be nil initially(for the first page).
query.observeSingleEvent(of: .value) { (snapshot) in
guard snapshot.exists(), let value = snapshot.value else {
completion(false, nil)
return
}
do {
let model = try FirebaseDecoder().decode([String: FeedsModel].self, from: value)
//We get the feeds in ['childAddedByAutoId key': model] manner. CodableFirebase decodes the data and we get our models populated.
var feeds = model.map { $0.value }
//Leaving the keys aside to get the array [FeedsModel]
feeds.sort(by: { (P, Q) -> Bool in P.timestamp > Q.timestamp })
//Sorting the values based on the timestamp, following recent first fashion. It is required because we may have lost the chronological order in the last steps.
if lastKey != nil { feeds = Array(feeds.dropFirst()) }
//Need to remove the first element(Only when the lastKey was not nil) because, it would be the same as the last one in the previous page.
completion(true, feeds)
//We get our data sorted and ready here.
} catch let error {
print("Error occured while decoding - \(error.localizedDescription)")
completion(false, nil)
}
}
}
Now, in our viewController, for the initial load, the function calls go like this in viewDidLoad. And the next pages are fetched when the tableView will display cells...
class FeedsViewController: UIViewController {
//MARK: - Properties
#IBOutlet weak var feedsTableView: UITableView!
var dataArray = [FeedsModel]()
var isFetching = Bool()
var previousKey = String()
var hasFetchedLastPage = Bool()
//MARK: - ViewController LifeCycle
override func viewDidLoad() {
super.viewDidLoad()
//Any other stuffs..
self.getFeedsWith(lastKey: nil) //Initial load.
}
//....
func getFeedsWith(lastKey: String?) {
guard !self.isFetching else {
self.previousKey = ""
return
}
self.isFetching = true
FeedsModel.getFeedsWith(lastKey: lastKey) { (status, data) in
self.isFetching = false
guard status, let feeds = data else {
//Handle errors
return
}
if self.dataArray.isEmpty { //It'd be, when it's the first time.
self.dataArray = feeds
self.feedsTableView.reloadSections(IndexSet(integer: 0), with: .fade)
} else {
self.hasFetchedLastPage = feeds.count < "YOUR FEEDS PER PAGE"
//To make sure if we've fetched the last page and we're in no need to call this function anymore.
self.dataArray += feeds
//Appending the next page's feed. As we're getting the feeds in the recent first manner.
self.feedsTableView.reloadData()
}
}
}
//MARK: - TableView Delegate & DataSource
//....
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if self.dataArray.count - 1 == indexPath.row && !self.hasFetchedLastPage {
let lastKey = self.dataArray[indexPath.row].postId
guard lastKey != self.previousKey else { return }
//Getting the feeds with last element's postId. (postId would be the same as a specific node in YourDatabase/Feeds).
self.getFeedsWith(lastKey: lastKey)
self.previousKey = lastKey ?? ""
}
//....
}