UIMenuController doesn't update menu for first time - ios

I have UITextView on which I want to add highlight as custom menu item. I have registered to following notification UIMenuControllerWillShowMenuNotification.
The method for the notification is something like this:
if textIsHighlighted {
let highlightMenuItem = UIMenuItem(title: "Highlight", action: Selector("highlightText"))
UIMenuController.sharedMenuController().menuItems = [highlightMenuItem]
}
else {
let highlightMenuItem = UIMenuItem(title: "Dehighlight", action: Selector("highlightText"))
UIMenuController.sharedMenuController().menuItems = [highlightMenuItem]
}
Although the first time the menucontroller fails to update even though it executes the part of code. It shows the last value. Where should I write this part of code as I feel that during willShow menuController it's already created and thus fails to update.

Hopefully you've solved this by now, but I've just figured this one out myself:
Other answers have said you can update the menu items by adding it when the UIMenuControllerWillShowMenuNotification is called, but this wasn't working for me (iOS 9, Swift 2).
Instead I implemented the UITextView delegate method: textViewDidChangeSelection and set the relevant menu items there:
func textViewDidChangeSelection(textView: UITextView) {
if self.currentSelectionIsInHighlightedRange() {
self.setUpUnhighlightMenuItem()
} else {
self.setUpHighlightMenuItem()
}
}
private func currentSelectionIsInHighlightedRange() -> Bool {
let allHighlightedRanges = self.document.highlightedRanges()
let selectedTextRange = self.documentView.textView.selectedRange
for range in allHighlightedRanges {
let intersectionRange = NSIntersectionRange(range, selectedTextRange)
if intersectionRange.length > 0 {
return true
}
}
return false
}

Related

Find UIRefreshControl in UI Test

I am wanting to test the existence of a UIRefreshControl inside a UI Test. I define my control all init:
itemRefreshControl.accessibilityIdentifier = "MyRefreshIndicator"
// allow UITest to find the refresh
if let refreshLabel = itemRefreshControl.subviews.first?.subviews.last as? UILabel {
refreshLabel.isAccessibilityElement = true
refreshLabel.accessibilityIdentifier = "MyRefreshLabel"
}
Then in my test case I have tried:
let refreshCtlQuery = NSPredicate(format: "label CONTAINS[c] 'Refreshing'")
let refreshControl = app.staticTexts.containing(refreshCtlQuery)
expectation(for: exists, evaluatedWith: refreshControl, handler: nil)
start.press(forDuration: 0, thenDragTo: finish)
print(app.debugDescription)
waitForExpectations(timeout: 5, handler: nil)
I also tried:
let refreshControl = app.staticTexts["MyRefreshLabel"]
and I tried:
let refreshControl = app.activityIndicators["MyRefreshIndicator"]
In all those cases I can see the test runner perform the drag and I see the refresh control in the UI, but the expectation always fails. It's almost like the test blocks until the refreshing is done and then checks for existence and it's not there. When I print out the view hierarchy, I can't find the UIRefreshControl's label. How best can I test this?
Indeed, while UIRefreshControl does its animation, the tests are hang up saying "Wait for <BUNDLE_IDENTIFIER> to idle".
You can swizzle XCUIApplicationProcess.waitForQuiescenceIncludingAnimationsIdle: to an empty method, so you can bypass this behaviour (based on this answer).
extension XCTestCase {
static var disabledQuiescenceWaiting = false
/// Swizzle `XCUIApplicationProcess.waitForQuiescenceIncludingAnimationsIdle(:)`
/// to empty method. Invoke at `setUpWithError()` of your test case.
func disableQuiescenceWaiting() {
// Only if not disabled yet.
guard Self.disabledQuiescenceWaiting == false else { return }
// Swizzle.
if
let `class`: AnyClass = objc_getClass("XCUIApplicationProcess") as? AnyClass,
let quiescenceWaitingMethod = class_getInstanceMethod(`class`, Selector(("waitForQuiescenceIncludingAnimationsIdle:"))),
let emptyMethod = class_getInstanceMethod(type(of: self), #selector(Self.empty))
{
method_exchangeImplementations(quiescenceWaitingMethod, emptyMethod)
Self.disabledQuiescenceWaiting = true
}
}
#objc func empty() {
return
}
}
Then call at the setup of your test case.
override func setUpWithError() throws {
disableQuiescenceWaiting()
}
You can mark the UIRefreshControl directly (I made this extension for convenience).
extension UIRefreshControl {
func testable(as id: String) -> UIRefreshControl {
self.isAccessibilityElement = true
self.accessibilityIdentifier = id
return self
}
}
// Create the instance like this.
UIRefreshControl().testable(as: "RefreshControl")
So you can nicely assert the existence (get from otherElements).
let refreshControlElement = app.otherElements["RefreshControl"]
XCTAssertTrue(refreshControlElement.waitForExistence(timeout: 1))

Listing scheduled notifications in a table view controller

I am trying to list all the notifications a user has created and scheduled in my app, similar to that of the list of alarms in the 'Clock' app from apple. However, each time I get the array of notifications and attempt to display them, they are not being correctly displayed all the time.
Each notification is repeated each day at the same time, so I am using UNUserNotificationCenter.current().getPendingNotificationRequests to get an array of the notifications. With this array of notifications, I iterate over each notification, create a new custom 'Reminder' object and add it to my array of 'Reminders' which I use when I display the notifications in the table view controller.
I do this every time using the viewWillAppear function.
Here is the code:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
generateReminders()
tableView.reloadData()
}
func generateReminders()
{
let center = UNUserNotificationCenter.current()
center.getPendingNotificationRequests { (notifications) in
for item in notifications {
if let trigger = item.trigger as? UNCalendarNotificationTrigger,
let triggerDate = trigger.nextTriggerDate() {
var withSound = true
if(item.content.sound != UNNotificationSound.default)
{
withSound = false
}
self.reminders.append(Reminder(identifier: item.identifier, time: triggerDate, message: item.content.body, withSound: withSound, isAPendingNotification: true))
}
}
self.remindersCount = notifications.count
}
}
When the cells are about to be displayed in the table view controller, I use the 'Reminder' array to customise each cell to display the information of the notification. This is all done in the 'cellForRowAt' function, code below.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Reminder", for: indexPath)
var text = ""
var detailText = ""
if(indexPath.row < remindersCount) {
let reminder = reminders[indexPath.row]
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm"
text = formatter.string(from: reminder.Time)
detailText = reminder.Message
}
cell.textLabel?.text = text
cell.detailTextLabel?.text = detailText
return cell
}
When a user selects another tab to view, I reset the 'reminders' object to be empty so that when they return to this tab, an updated array of notifications is displayed, code below.
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
reminders = [Reminder]()
remindersCount = 0
tableView.setNeedsDisplay()
}
The issue I am facing is this is extremely inconsistent, sometimes all the notifications are displayed, sometimes only some are displayed and other times none of them are displayed. However, each time I print out the count of the number of notifications in the UNUserNotificationCenter.current().getPendingNotificationRequests method it is always the correct number. Furthermore, whenever I click on a cell that should contain information about a notification, the information is there, it is just not being displayed.
Here is a short video of these issues.
https://imgur.com/1RVerZD
I am unsure how to fix this, I have attempted to run the code on the main queue and on the global queue with the quality of service set to '.userInteractive' as shown below, but, still no dice.
let center = UNUserNotificationCenter.current()
let dq = DispatchQueue.global(qos: .userInteractive)
dq.async {
center.getPendingNotificationRequests { (notifications) in
for item in notifications {
if let trigger = item.trigger as? UNCalendarNotificationTrigger,
let triggerDate = trigger.nextTriggerDate() {
var withSound = true
if(item.content.sound != UNNotificationSound.default)
{
withSound = false
}
self.reminders.append(Reminder(identifier: item.identifier, time: triggerDate, message: item.content.body, withSound: withSound, isAPendingNotification: true))
}
}
self.remindersCount = notifications.count
}
}
A small application of this issue occurring can be downloaded from this Github repository.
https://github.com/AlexMarchant98/LitstingNotificationsIssue
There are some issues in your code of tableview as listed below.
You used static cells in your tableview which is not the proper way if you have dynamic rows.
Suggestion : Use Dynamic Prototype Cell for your tableview.
remindersCount not required at all as its already there in your array count.
Suggestion : Use self.reminders.count for array count.
unwindToRemindersTableViewController() method have generateReminders() call which is not required as viewWillAppear() will call when view dismiss.
Suggestion : Check ViewController life cycle you will get proper idea how to reload data.
I have updated some code in your sample project.
Please find updated code here.
Github updated demo
Hope this will helps!
The problem with your code is the timing when tableView.reloadData() gets executed.
UNUserNotificationCenter.current().getPendingNotificationRequests() is a asynchronous call and therefore the reminders array gets filled after tableView.reloadData() was called.
Moving tableView.reloadData() to the end of the callback-block of getPendingNotificationRequests() should fix your issue. (And don't forget to trigger the reloadData() from the main thread)
func generateReminders()
{
let center = UNUserNotificationCenter.current()
let dq = DispatchQueue.global(qos: .userInteractive)
dq.async {
center.getPendingNotificationRequests { (notifications) in
for item in notifications {
if let trigger = item.trigger as? UNCalendarNotificationTrigger,
let triggerDate = trigger.nextTriggerDate() {
var withSound = true
if(item.content.sound != UNNotificationSound.default)
{
withSound = false
}
self.reminders.append(Reminder(identifier: item.identifier, time: triggerDate, message: item.content.body, withSound: withSound, isAPendingNotification: true))
}
}
self.remindersCount = notifications.count
DispatchQueue.main.async {
self.tableView.reloadData() // <---------
}
}
}
}

Trigger UIAlertViewController Based on Time

I have UITable to display different animals. When you select a cell in the table, a new view controller with a large UIImage is pushed. Currently, when you zoom in on the image, a UIAlertView is triggered that asks the user if they would like to download hi res images. If they click yes, the "hi-res-flag" is set to "yes" in user defaults and they no longer see the pop up. However, if they select no, the hi-res-flag will continue to pop up each time they zoom in on a photo.
Instead, if they answer no, I would like to have this flag pop up occasionally. Not every time the click a cell in the species table, nor every time they open the app. Something more like once or twice a month. Is there a way to use time in the logic of an iOS app? For instance, erase the value set for "high-res-flag" (if already equals 'no') in user defaults, once a month?
Store the time you showed the alert last in the user preferences, and then check that value every time before you present the alert whether a certain time has passed.
I have written a time checker class that does the job. The code is in Swift. You can use it from your Objective-C code as well. You can find this code in gist here.
Solution
Below, you use the viewWillAppear delegate method to see if the hiResFlag is existing. If it is present and false, then you check to see if you can display the popup:
import UIKit
class ImageViewController: UIViewController {
//Whenever you enter the Image View Controller, you check whether to show popup or not
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if let hiResFlag = hiResFlag {
if hiResFlag == false {
if PopUpTimeChecker.shouldShowPopUp() {
self.presentAlert()
}
}
}
}
func presentAlert() {
let alert = UIAlertController.init(title: nil, message: "Show Pop up", preferredStyle: .alert)
let action = UIAlertAction.init(title: "Yeahh!", style: .default, handler: nil)
alert.addAction(action)
self.present(alert, animated: true, completion: nil)
}
}
The following code implements the time-checking algorithm. Edit popUpTimeInterval below for setting your minimum time. Right now, it is set to be 15 days (in seconds). Once in every 15 days the pop-up will be shown when you call the shouldShowPopUp method.
import UIKit
//Below 4 variables, I have made them Global. No need to make them global in your case
#objc var popUpTimeInterval: UInt64 = 1296000 //15 days in seconds
#objc var hiResFlag: Bool? {
get {
return UserDefaults.standard.object(forKey: "HiResFlag") as? Bool
}
set {
UserDefaults.standard.setValue(newValue, forKey: "HiResFlag")
}
}
#objc var isFirstTimePopUp: Bool {
get {
let value = UserDefaults.standard.object(forKey: "IsFirstTimePopUp")
return value == nil ? true : value as! Bool
}
set {
UserDefaults.standard.setValue(newValue, forKey: "IsFirstTimePopUp")
}
}
#objc var lastDateOfPopUp: Date? {
get {
return UserDefaults.standard.object(forKey: "LastDateOfPopUp") as? Date
}
set {
UserDefaults.standard.setValue(newValue, forKey: "LastDateOfPopUp")
}
}
#objc class PopUpTimeChecker {
#objc static fileprivate func setLastPopUpDate() {
//Setting current date to last shown pop up date
lastDateOfPopUp = Date()
}
#objc static fileprivate func timeIntervalSinceLastPopUp() -> UInt64 {
//Returning how much time (in seconds) has passed from last popup date until now
return UInt64(Date().timeIntervalSince(lastDateOfPopUp!))
}
#objc static func shouldShowPopUp() -> Bool {
//We proceed further only if we have the last date when pop up was displayed, else we create and set it as the current date
if let _ = lastDateOfPopUp {
let timeInterval = timeIntervalSinceLastPopUp()
if timeInterval > popUpTimeInterval {
self.setLastPopUpDate()
return true //Show pop up
} else {
if isFirstTimePopUp {
//If this is the first time, you just allow the pop up to show, don't allow otherwise
isFirstTimePopUp = false
return true
} else {
return false
}
}
} else {
self.setLastPopUpDate() //Since we don't have a last date, we set it here for starting off
return self.shouldShowPopUp() //Recursively call method
}
}
}

View is not being updated when calling UIButton.isHidden = true/false

I am using xcode 8.2 and swift to make a simple application.
I have added a UIButton to my View using the Interface Builder.
I have added the appropriate outlets for the button:
#IBOutlet weak var myBtn: UIButton!
I want this button to be hidden on start so in viewDidLoad I am setting is to Hidden. Like this:
override func viewDidLoad() {
super.viewDidLoad()
...
myBtn.isHidden = true
...
mqttConfig = MQTTConfig(clientId: "iphone7", host: "192.xx.xx.150", port: 18xx, keepAlive: 60)
mqttConfig.onMessageCallback = { mqttMessage in
if ( mqttMessage.topic == "status" ) {
if ( mqttMessage.payloadString?.localizedStandardContains("show") )! {
self.showButton = true
} else if ( mqttMessage.payloadString?.localizedStandardContains("hide") )! {
self.showButton = false
}
self.showHideSeatButtons()
} else {
// something to do in case of other topics
}
}
Later in the code I have a function to show/hide this button.
func showHideButton(){
if ( self.showButton ) {
print("button enabled!")
myBtn.isHidden = false
} else {
print("button disabled!")
myBtn.isHidden = true
}
}
When I call this function (by receiving a certain message using MQTT) I get the print outs but I don't see the button.
If I press where I know the button is, then the button gets shown.
Any idea what could be going on here? I have spent and hour googling this now! Please don't suggest object-c way of solving this issue, as I don't know object-c.
In onMessageCallback block
Replace following line
self.showHideSeatButtons()
with
DispatchQueue.main.async {
self.showHideSeatButtons()
}
Note: UI related changes/updates must be handled by main queue (thread).
Since you're calling a service it's possible you're not working in the same thread. Try this:
func showHideButton(){
DispatchQueue.main.async {
if (self.showButton ) {
print("button enabled!")
self.myBtn.isHidden = false
} else {
print("button disabled!")
self.myBtn.isHidden = true
}
}
}
try this
myBtn.isHidden = true
myBtn.alpha = 0

automatically update label in Swift by checking the status

I'm new to programming with iOS and Swift. I have a label that should automatically be updated when the status of the StreamingKit Framework changes.
Currently the label only changes when I press a different button (it calls the self.updateView() function again), but I want it to happen automatically when the status has changed, not by pressing a different button.
Here is the code:
func updateView() {
dispatch_async(dispatch_get_main_queue()) {
if let label = self.bufferingLabel, let button = self.playerButton{
if let audioPlayer = self.player{
if(audioPlayer.state == STKAudioPlayerStateBuffering)
{
self.playerState = "Loading"
button.setImage(self.image1, forState: UIControlState.Normal)
}
else(audioPlayer.state == STKAudioPlayerStatePlaying)
{
self.playerState = self.defaultPlayerState
self.playerState = "Playing"
button.setImage(self.image1, forState: UIControlState.Normal)
}
label.text = self.playerState!
}
}
So when I press this button, it gives the first state (which is loading), but after that is done (status changed), the label should change to play, but it doesn't.
#IBAction func pressStart(sender: AnyObject) {
//declare path to streaming
let filePath = "http://mp3.streampower.be/mnm-high.mp3"
if player == nil {
self.player = STKAudioPlayer()
self.player?.delegate = self
}
if let audioPlayer = player{
if (audioPlayer.state == STKAudioPlayerStateReady) ||
(audioPlayer.state == STKAudioPlayerStateStopped){
audioPlayer.play(filePath)
}else{
audioPlayer.stop()
}
}
self.updateView()
}
Some of the delegates I tried as well
func audioPlayer(audioPlayer: STKAudioPlayer!, didStartPlayingQueueItemId queueItemId: NSObject!) {
self.updateView()
}
func audioPlayer(audioPlayer: STKAudioPlayer!, didFinishBufferingSourceWithQueueItemId queueItemId: NSObject!) {
self.updateView()
}
func audioPlayer(audioPlayer: STKAudioPlayer!, stateChanged state: STKAudioPlayerState, previousState: STKAudioPlayerState) {
self.updateView()
}
You should look and use the delegate property of the STKAudioPlayer.
I'm not familiar with it, but generally in the iOS world, that's how you receive events from an object (the object delegates some stuff to the object of your choice).
You can use NSNotifications to send a signal whenever the streaming kit changes. Then you can add an observer to your UI view controller that will trigger a function whenever it receives a signal.
If you want to try out functional reactive programming you can also use The Reactive Cocoa framework to do a similar signal observer pattern. And finally, there is plain old key-value observation. One of these options should be able to help you.

Resources