I subclassed UICollectionViewLayout in order to create a calendar. I would like to give users the ability to change some settings like the number of days to be shown on the screen (7 by default).
I save daysToShow in UserDefaults. Whenever the UIStepper value is changed it calls this method:
func stepperValueChanged(sender:UIStepper){
stepperValue = String(Int(sender.value))
valueLabel.text = String(Int(sender.value))
UserDefaults.standard.set(String(Int(sender.value)), forKey: "daysToShow")
NotificationCenter.default.post(name: Notification.Name(rawValue: "calendarSettingsChanged"), object: nil, userInfo: nil)
}
So after I save the new value in UserDefault, I post a notification which then calls reloadForSettingsChange (which is actually getting called as I set a breakpoint here):
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(reloadForSettingsChange(notification:)), name: NSNotification.Name(rawValue: "calendarSettingsChanged"), object: nil)
// other code.....
}
func reloadForSettingsChange(notification:NSNotification){
// here I save the user setting in a variable declared in my custom UICollectionViewLayout
self.calendarView.daysToShow = Int(UserDefaults.standard.string(forKey: "daysToShow")!)
self.calendarView.daysToShowOnScreen = Int(UserDefaults.standard.string(forKey: "daysToShow")!)
self.calendarView.forceReload(reloadEvent: true)
}
func forceReload(reloadEvent:Bool){
DispatchQueue.main.async {
if reloadEvent{
self.groupEventsByDays()
self.weekFlowLayout?.invalidateCacheLayout()
self.collectionView.reloadData()
}
}
}
func invalidateCacheLayout(){
self.needsToPopulateAttributesForAllSections = true
self.cachedDayDateComponents?.removeAllObjects()
self.cachedStartTimeDateComponents?.removeAllObjects()
self.cachedEndTimeDateComponents?.removeAllObjects()
self.cachedCurrentDateComponents?.removeAllObjects()
self.cachedEarliestHour = Int.max
self.cachedLatestHour = Int.min
self.cachedMaxColumnHeight = CGFloat.leastNormalMagnitude
self.cachedColumnHeights?.removeAllObjects()
self.cachedEarliestHours?.removeAllObjects()
self.cachedLatestHours?.removeAllObjects()
self.itemAttributes?.removeAllObjects()
self.allAttributes?.removeAllObjects()
_layoutAttributes.removeAll()
self.invalidateLayout()
}
The problem is that the layout is not being updated (invalidated?) until I rotate the screen device (i.e from landscape to portrait). The function I use for the rotation calls exactly the same method I call in reloadForSettingsChange so I don't understand why it works when I rotate the screen and not before:
func rotated() {
switch UIDevice.current.orientation {
case .landscapeLeft, .landscapeRight:
calendarView.forceReload(reloadEvent: true)
default:
calendarView.forceReload(reloadEvent: true)
}
}
I found the solution: I am now calling setNeedsLayout():
func reloadForSettingsChange(notification:NSNotification){
self.calendarView.daysToShow = Int(UserDefaults.standard.string(forKey: "daysToShow")!)
self.calendarView.daysToShowOnScreen = Int(UserDefaults.standard.string(forKey: "daysToShow")!)
self.calendarView.forceReload(reloadEvent: true)
self.calendarView.setNeedsLayout() // this line has been added
}
I had initially used layoutSubviews() which worked but Apple Documentation says:
You should not call this method directly. If you want to force a
layout update, call the setNeedsLayout() method instead to do so prior
to the next drawing update. If you want to update the layout of your
views immediately, call the layoutIfNeeded() method.
I tried setNeedsLayout() which seemed to be the right method to call (?!?) but since it wasn't working I used setNeedsLayout():
Call this method on your application’s main thread when you want to
adjust the layout of a view’s subviews. This method makes a note of
the request and returns immediately. Because this method does not
force an immediate update, but instead waits for the next update
cycle, you can use it to invalidate the layout of multiple views
before any of those views are updated. This behavior allows you to
consolidate all of your layout updates to one update cycle, which is
usually better for performance.
Related
I have a collection view inside a table view. There are two plus minus buttons in collection view cell. Now i have to update a label on the plus minus buttons action which is outside of table view. Thanks in advance.
I have to update a Slot: (label) by clicking on plus minus button.
I tried something like this with the delegate protocol.
I declare a delegate in my collection view class.
protocol SlotsCollectionViewCellDelegate: NSObjectProtocol {
func didTapOnIncrement(Int: Int)
func didTapOnDecrement(Int: Int)
}
//after that,
var delegate: SlotsCollectionViewCellDelegate?
#IBAction func plusBtnAction(_ sender: Any) {
self.delegate?.didTapOnIncrement(Int: cartCount)
}
#IBAction func minusBtnAction(_ sender: Any) {
delegate?.didTapOnDecrement(cell: self)
}
And in my Main View Controller
extension MainViewController: SlotsCollectionViewCellDelegate {
func didTapOnIncrement(Int: Int) {
cartSlot_lbl.text = Int.description
}
func didTapOnDecrement(Int: Int) {
cartSlot_lbl.text = Int.description
}
}
If I understood correctly, each time you push + or - you want to update slot label. In my opinion the easiest and fastest way to achieve this it's using NotificationCenter.default.post
In your collection view cell on button action write:
NotificationCenter.default.post(name: Notification.Name("postAction"), object: numberToIncreaseOrDecrease)
In your MainViewController where you have the slot label, add this code in view did load:
NotificationCenter.default.addObserver(self, selector: #selector(updateSlotValue(_:)), name: NSNotification.Name("postAction"), object: nil)
And out from the view did load add this function:
#objc func updateSlotValue(_ notification: Notification) {
let value = notification.object as! Int
cartSlot_lbl.text.text = value
}
I think delegates are the right choice for that. If it didn't work, please explain why and show some code, you probably forgot to set a delegate reference.
Anyway, here's some more thoughts:
You could use a Reactive Pattern, so that you create a Relay to store your current values, manipulate them by providing input (times etc.) and subscribe to them from the Class where the "Spot:" Label is implemented. Whenever your model changes, your Spot Label will also be changed.
You could also implement something using Notifications. Basically speaking, the difference to using a reactive pattern is not that big, you simply have to care about the "notify" part yourself. Assuming you have something like a Singleton Pattern applied where you store your entire State (selected dates/times, slots etc.), you could do that like this:
extension Notification.Name {
static let modelDidChange = Notification.Name("modelDidChange")
}
// where your model lies
struct YourModel {
var slots: Int = 0
static var singletonInstance: YourModel = YourModel() {
// use the didSet block to react to changes made to the model
didSet {
// send a notification so all subscriber know something has changed
NotificationCenter.default.post(.modelDidChange)
}
}
}
class YourViewControllerWhereTheLabelIs: UIViewController {
// ...
var slotLabel: UILabel?
// ...
init() {
// wherever you initialize your viewcontroller,
// you could also do it in viewWillAppear
// subscribe to the notification to react to changes
NotificationCenter.default.addObserver(self, selector: #selector(modelDidChange), name: .modelDidChange, object: nil)
}
deinit {
// just don't forget to unsubscribe
NotificationCenter.default.removeObserver(self)
}
#objc func modelDidChange() {
// update your label here, this is called whenever YourModel.singletonInstance is changed
self.slotLabel?.text = YourModel.singletonInstance.slots
}
}
Hope that helps you or gives you an idea. If I can be of more help just let me know.
I'm using the delegation pattern for my CoreBluetooth based app. I have a main ViewController that is a delegate to my BLEHandler class. I'm updating a button based on the response I get from following delegate method:
func acIsOn(error: NSError?) {
if error == nil{
pushButton.setImage(UIImage(named: "state4"), for: .normal)
}
}
It works fine when my delegate controller class is in the foreground but when I move to another ViewController and calls a method of handler class that in response calls the delegate method above, the button on the image is not updated.
Here's what I have already tried:
Wrapping the statement in DispatchQueue.main.async{}
Calling pushButton.setNeedsLayout() and setNeedsDisplay()
But none of it worked. ,
Also I made sure that this method was being called when the ViewController is not in the foreground.
Edit 1: I'm more interested in learning about the limitation that is not allowing this to happen, I'm not looking for hacks/tricks to bypass this.
Edit 2: As mentioned by Shoazab, button.setBackgroundImage() is working when the VC is in background. Still curious why button.setImage doesn't work in background but it does in when VC is on top.
You are trying to changed an UI element on a ViewController which is not currently displayed. It will update values but no refresh on UI will be executed.
I think that calling SetNeedsDisplay on method viewWillAppear on your ViewController will fix your problem.
You can also use a variable Image and update the button Image when the controller is displayed again.
Move the UI update code to the viewWillAppear like this:
class MainViewController: UIViewController, BLEHandler {
var isUIUpdateNeeded = false
//Define your UI outlets or proeprties
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func viewWillAppear(_ animated: Bool) {
if isUIUpdateNeeded {
updateUI()
}
}
func acIsOn(error: NSError?) {
if error == nil{
pushButton.setImage(UIImage(named: "state4"), for: .normal)
}
}
func updateUI() {
//do your UI changes here
pushButton.setImage(UIImage(named: "state4"), for: .normal)
}
}
Call btn.setBackgroundImage() it will set it
Is it possible to move the keyboard up so it doesn't cover the UITabViewController's TabBar?
Update after being given more context in comments
If your main concern is letting the user dismiss the keyboard, there are some well known patterns that are commonly applied on the platform:
Assumption regarding UI (derived from your comment):
- UITableView as main content
To make the keyboard dismissible, you can utilise a property on UIScrollView called .keyboardDismissMode. (UITableView is derived from UIScrollView, so it inherits the property.)
The default value for this property is .none. Change that to either .onDrag or .interactive. Consult the documentation for differences between the latter two options.
Behind the scenes, UIKit sets up a connection between the UIScrollView instance and any incoming keyboard. This allows the user to "swipe away" the keyboard by interacting with the scroll view.
Note that in order for this feature to work, your UIScrollView needs to be scrollable. To understand what 'scrollable' means in this context, please see this gist.
If your tableView has very few or no rows, it is likely not natively scrollable. To account for that, set tableView.alwaysBounceVertical = true. This will make sure your users can dismiss the keyboard regardless of the number of rows in the table.
Most of the popular apps handling keyboard dismissal also make it possible to dismiss the keyboard simply by tapping the content partially overlapped by it (in your case, the tableView). To enable this, you would simply have to install a UITapGestureRecognizer on your view and dismiss the keyboard in its action method:
class MyViewController: UIViewController {
func viewDidLoad() {
super.viewDidLoad()
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap))
view.addGestureRecognizer(tapRecognizer)
}
}
//MARK: - Tap handling
fileprivate extension MyViewController {
#objc func handleTap() {
if searchBar.isFirstResponder {
searchBar.resignFirstResponder()
}
// Alternative
// view.endEditing(true)
}
}
// -
Old answer
Yes, you can actually do this without using private API.
Disclaimer
You should really think about whether you actually want to do this. Opening the keyboard in virtually every use case should create a new "context" of editing which modally "blocks" other contexts (such as the navigation context provided by UITabBarController and its UITabBar). I guess one could make the point that users are able to leave an editing context by interacting with a potentially present UINavigationBar which is usually not blocked by keyboards. However, this is a known interaction throughout the system. Not blocking a UITabBar or UIToolbar while showing the keyboard on the other hand, is not. That being said, use the code below to move the keyboard up, but critically review the UX you are creating. I'm not to say it does never make sense to move the keyboard up, but you should really know what you're doing here. To be honest, it also looks kind of iffy, having the keyboard float above the tab bar.
Code
extension Sequence {
func last(where predicate: (Element) throws -> Bool) rethrows -> Element? {
return try reversed().first(where: predicate)
}
}
// Using `UIViewController` as an example. You could and actually should factor this logic out.
class MyViewController: UIViewController {
deinit {
NotificationCenter.default.removeObserver(self)
}
func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: .UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(notification:)), name: .UIKeyboardWillHide, object: nil)
}
}
//MARK: - Keyboard handling
extension MyViewController {
private var keyboardOffset: CGFloat {
// Using a fixed value of `49` here, since that's what `UITabBar`s height usually is.
// You should probably use something like `-tabBarController?.tabBar.frame.height`.
return -49
}
private var keyboardWindowPredicate: (UIWindow) -> Bool {
return { $0.windowLevel > UIWindowLevelNormal }
}
private var keyboardWindow: UIWindow? {
return UIApplication.shared.windows.last(where: keyboardWindowPredicate)
}
#objc fileprivate func keyboardWillShow(notification: Notification) {
if let keyboardWindow = keyboardWindow {
keyboardWindow.frame.origin.y = keyboardOffset
}
}
#objc fileprivate func keyboardWillHide(notification: Notification) {
if let keyboardWindow = keyboardWindow {
keyboardWindow.frame.origin.y = 0
}
}
}
// -
Caution
Note that if you are using the .UIKeyboardWillShow and .UIKeyboardWillHide notifications to account for the keyboard in your view (setting UIScrollView insets, for example), you would have to also account for any additional offset by which you move keyboard window.
This works and is tested with iOS 11. However, there is no guarantee that the UIKit team won't change the order of windows or something else that breaks this in future releases. Again, you are not using any private API, so AppStore review should not be in danger, but you are doing something that you're not really supposed to do with the framework, and that can always come around and bite you later on.
How to redraw non-visible UICollectionViewCell's ready for when reuse occurs???
One approach I thought of was per the code in the Layout Cell prepareForReuse function, however whilst it works it non-optimal as it causes more re-drawing then required.
Background: Need to trigger drawRect for cells after an orientation change that are not current visible, but pop up to be used and haven't been redraw, so so far I can only see that prepareForReuse would be appropriate. Issue is I'm re-drawing all "reuse" cells, whereas I really only want to redraw those that initially pop up that were created during the previous orientation position of the device.
ADDITIONAL INFO: So currently I'm doing this:
In ViewController:
override func viewWillLayoutSubviews() {
// Clear cached layout attributes (to ensure new positions are calculated)
(self.cal.collectionViewLayout as! GCCalendarLayout).resetCache()
self.cal.collectionViewLayout.invalidateLayout()
// Trigger cells to redraw themselves (to get new widths etc)
for cell in self.cal?.visibleCells() as! [GCCalendarCell] {
cell.setNeedsDisplay()
}
// Not sure how to "setNeedsDisplay" on non visible cells here?
}
In Layout Cell class:
override func prepareForReuse() {
super.prepareForReuse()
// Ensure "drawRect" is called (only way I could see to handle change in orientation
self.setNeedsDisplay()
// ISSUE: It does this also for subsequent "prepareForReuse" after all
// non-visible cells have been re-used and re-drawn, so really
// not optimal
}
Example of what happens without the code in prepareForReuse above. Snapshot taken after an orientation change, and just after scrolling up a little bit:
I think I have it now here:
import UIKit
#IBDesignable class GCCalendarCell: UICollectionViewCell {
var prevBounds : CGRect?
override func layoutSubviews() {
if let prevBounds = prevBounds {
if !( (prevBounds.width == bounds.width) && (prevBounds.height == bounds.height) ) {
self.setNeedsDisplay()
}
}
}
override func drawRect(rect: CGRect) {
// Do Stuff
self.prevBounds = self.bounds
}
}
Noted this check didn't work in "prepareForReuse" as at this time the cell had not had the rotation applied. Seems to work in "layoutSubviews" however.
You can implement some kind of communication between the cells and the view controller holding the collection view ( protocol and delegate or passed block or even direct reference to the VC ). Then You can ask the view controller for rotation changes.
Its a bit messy, but if You have some kind of rotation tracking in Your view controller You can filter the setNeedsDisplay with a simple if statement.
I had similar challenged updating cells that were already displayed and off the screen. While cycling through ALLL cells may not be possible - refreshing / looping through non-visible ones is.
IF this is your use case - then read on. Pre - Warning - if you're adding this sort of code - explain why you're doing it. It's kind of anti pattern - but can help fix that bug and help ship your app albeit adding needless complexity. Don't use this in multiple spots in app.
Any collectionviewcell that's de-initialized (off the screen and being recylced) should be unsubscribed automatically.
Notification Pattern
let kUpdateButtonBarCell = NSNotification.Name("kUpdateButtonBarCell")
class Notificator {
static func fireNotification(notificationName: NSNotification.Name) {
NotificationCenter.default.post(name: notificationName, object: nil)
}
}
extension UICollectionViewCell{
func listenForBackgroundChanges(){
NotificationCenter.default.removeObserver(self, name: kUpdateButtonBarCell, object: nil)
NotificationCenter.default.addObserver(forName:kUpdateButtonBarCell, object: nil, queue: OperationQueue.main, using: { (note) in
print( " contentView: ",self.contentView)
})
}
}
override func collectionView(collectionView: UICollectionView!, cellForItemAtIndexPath indexPath: NSIndexPath!) -> UICollectionViewCell! {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("die", forIndexPath: indexPath) as UICollectionViewCell
cell.listenForBackgroundChanges()
return cell
}
// Where appropriate broadcast notification to hook into all cells past and present
Notificator.fireNotification(notificationName: kUpdateButtonBarCell)
Delegate Pattern
It's possible to simplify this.... an exercise for the reader. just do not retain the cells (use a weak link) - otherwise you'll have memory leaks.
I'm using YALFoldingTabBar for a project https://github.com/Yalantis/FoldingTabBar.iOS
I want to change its behavior a bit, by showing the current ViewController as the centerButtonImage, instead of a plus/cross sign.
I've tried to override didSelectItem like this:
override func tabBar(tabBar: UITabBar, didSelectItem item: UITabBarItem!) {
switch(self.selectedIndex)
{
case 0:
self.centerButtonImage = UIImage(named: "whatever")
default:
//Change image
}
switch (self.selectedViewController)
{
case 0 as GameViewController:
//Change image
default:
//Change image
}
}
The code below is in my CustomTabBar, subclassing YALFoldingTabBarController.
But it doesn't seem to work that way. I've also tried to manipulate the tabBar in the specific UIViewControllers's viewDidLoad or viewDidAppear but it seems like the tabBar is set only.
Is this possible, and if so, what am I doing wrong?
EDIT: It seems like the method is not called, when changing viewController.
I never got didSelectItem to work, and neither the UITabBarControllerDelegate´s equivalent. However, YALFoldingTabBar have delegate methods tabBarViewWillExpand and tabBarViewWillCollapse and I used these along with tabBarView.layoutSubviews() to solve my problem. It's not very beautiful, but works as intended.
func tabBarViewWillExpand(tabBarView: YALFoldingTabBar!) {
self.centerButtonImage = UIImage(named: "plus_icon")
//Helper method from other thread, link below
performBlock({ () -> Void in
tabBarView.layoutSubviews()
}, afterDelay: 0.25)
}
How do you trigger a block after a delay, like -performSelector:withObject:afterDelay:?