Customise UITabBarController.moreNavigationController UITableViewCells - swift

As stated in the title, I'm trying to customise the cells in the moreNavigationController. I've read a number of SO posts asking a similar question but most are outdated and seem to be hacks.
I'm assuming this is a very common situation so there must be a 'tidy' way of doing this surely?
I've looked into overriding one of the tableviews delegate methods but I couldn't figure that out assuming it's even possible. I've also tried the following:
override func viewDidLoad() {
super.viewDidLoad()
let moreNavController = self.moreNavigationController
if let moreTableView = moreNavController.topViewController?.view as? UITableView {
for cell in moreTableView.visibleCells {
cell.textLabel?.textColor = AppDarkColor
cell.imageView?.image = cell.imageView?.image?.imageWithRenderingMode(.AlwaysTemplate)
}
}
}
But moreTableView.visibleCells is empty at least when the UITabBarController is loaded. Maybe I need to move this somewhere else but I'm not sure where or if this is the right approach.

Experimented putting the code in viewWillAppear. It works for me (at least). Hope it works for you!

Related

Subclassed NumberFormatter in NSTableView not formatting

I have subclassed a NumberFormatter for my specific use and it's working great when I call it from code. Here's the subclass:
class MyNumberFormatterUnitPrice: NumberFormatter {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
override init() {
super.init()
setup()
}
private func setup() {
self.format = "#,##0.00####;(#,##0.00####)"
self.numberStyle = .currencyAccounting
}
}
I'm doing this because, while it's possible to set the positive and negative formats in the storyboard, you can't set those and "currencyAccounting". However, when I create a NumberFormatter in the storyboard, choose this subclass and then put it under my text cell for the column I would like to format, it appears to get overridden within:
tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView?
In here, I'm currently setting the cell by this logic:
let cellIdentifier = tableColumn!.identifier.rawValue
if let cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(cellIdentifier), owner: nil) as? NSTableCellView {
...
I believe that I'm getting the tableviewcell from the tableview by its identifier and not creating a new one, so that should have its textfield along with it. I read the documentation and this appears to be the right way to do this. If I set the numberformatter after I've gotten the cell here, it works. But I don't want to do it this way because I don't want a giant case statement to set specific cell properties - I'd like to do this in the storyboard editor.
Any advice on how to use the storyboard editor to set the numberformatter?
Edit: Further to this, I have put a breakpoint inside where I am setting the stringvalue of the textfield, and the text is being set as follows:
textfield.stringValue = text
Looking at where the number formatter is in the debugger, it's set at the table column level - that doesn't seem right. But it's definitely still there and hasn't been written over somehow.
(lldb) e ((tableColumn?.dataCell as! NSTextFieldCell).formatter as! NumberFormatter).format
(String) $R14 = "#,##0.00####;0.00;(#,##0.00####)"
And the textfield's formatter is nil... weird.
(lldb) e textfield.formatter
(Formatter?) $R26 = nil
I'm going to go back and check the storyboard to see if maybe I dropped the formatter in the wrong place.
OK that got it. I'm obviously a noob to MacOS development and I just trusted that dropping the number formatter into the column I wanted was going to put it at the cell level... that's NOT true!.
In this image you can see the right and the wrong way to do this if you're expecting to format the cell. The right way is highlighted. You have to open the hierarchy up right down to the text field for the cell. The wrong way is above and you'll see it under the "text cell" which is actually the text cell for the column.
Thanks for everyone who stuck with me and tried help me on this. I hope this answer helps others in the future avoid several days of frustration like I just had!
Also as #Willeke points out, there's no real need to subclass the numberformatter if all you're doing is setting these properties. I'm going to do it because I have lots of cells that I want to share a common format but if all you're doing is formatting one cell, it's not needed.

Prefer Large Titles and RefreshControl not working well

I am using this tutorial to implement a pull-to-refresh behavior with the RefreshControl. I am using a Navigation Bar. When using normal titles everything works good. But, when using "Prefer big titles" it doesn't work correctly as you can see in the following videos. Anyone knows why? The only change between videos is the storyboard check on "Prefer Large Titles".
I'm having the same problem, and none of the other answers worked for me.
I realised that changing the table view top constraint from the safe area to the superview fixed that strange spinning bug.
Also, make sure the constant value for this constraint is 0 🤯.
At the end what worked for me was:
In order to fix the RefreshControl progress bar disappearing bug with large titles:
self.extendedLayoutIncludesOpaqueBars = true
In order to fix the list offset after refreshcontrol.endRefreshing():
let top = self.tableView.adjustedContentInset.top
let y = self.refreshControl!.frame.maxY + top
self.tableView.setContentOffset(CGPoint(x: 0, y: -y), animated:true)
If you were using tableView.tableHeaderView = refreshControl or tableView.addSubView(refreshControl) you should try using tableView.refreshControl = refreshControl
It seems there are a lot of different causes that could make this happen, for me I had a TableView embedded within a ViewController. I set the top layout guide of the tableview to the superview with 0. After all of that still nothing until I wrapped my RefreshControl end editing in a delayed block:
DispatchQueue.main.async {
if self.refreshControl.isRefreshing {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0, execute: {
self.refreshControl.endRefreshing()
})
}
}
The only working solution for me is combining Bruno's suggestion with this line of code:
tableView.contentInsetAdjustmentBehavior = .always
I've faced the same problem. Call refreshControl endRefreshing before calling further API.
refreshControl.addTarget(controller, action: #selector(refreshData(_:)), for: .valueChanged)
#objc func refreshData(_ refreshControl: UIRefreshControl) {
refreshControl.endRefreshing()
self.model.loadAPICall {
self.tableView.reloadData()
}
}
The only solution that worked for me using XIBs was Bruno's one:
https://stackoverflow.com/a/54629641/2178888
However I did not want to use a XIB. I struggled a lot trying to make this work by code using AutoLayout.
I finally found a solution that works:
override func loadView() {
super.loadView()
let tableView = UITableView()
//configure tableView
self.view = tableView
}
I had this issue too, and i fixed it by embedded my scrollView (or tableView \ collectionView) inside stackView, and it's important that this stackView's top constraint will not be attached to the safeArea view (all the other constraints can). the top constraint should be connect to it's superview or to other view.
I was facing the same issue for very long, the only working solution for me was adding refresh control to the background view of tableview.
tableView.backgroundView = refreshControl
Short Answer
I fixed this by delaying calling to API until my collection view ends decelerating
Long Answer
I notice that the issue happens when refresh control ends refreshing while the collection view is still moving up to its original position. Therefore, I delay making API call until my collection view stops moving a.k.a ends decelerating. Here's a step by step:
Follow Bruno's suggestion
If you set your navigation bar's translucent value to false (navigationBar.isTranslucent = false), then you will have to set extendedLayoutIncludesOpaqueBars = true on your view controller. Otherwise, skip this.
Delay api call. Since I'm using RxSwift, here's how I do it.
collectionView.rx.didEndDecelerating
.map { [unowned self] _ in self.refreshControl.isRefreshing }
.filter { $0 == true }
.subscribe(onNext: { _ in
// make api call
})
.disposed(by: disposeBag)
After API completes, call to
refreshControl.endRefreshing()
Caveat
Do note that since we delay API call, it means that this whole pull-to-refresh process is not as quick as it could have been done without the delay.
Unfortunately, no advice helped. But I found a solution that helped me. Setting the transparency of the navigation bar helped.enter image description here
Problem can be solved if add tableview or scroll view as root view in UIViewController hierarchy (like in UITableViewController)
override func loadView() {
view = customView
}
where customView is UITableView or UICollectionView

Programmatic beginRefreshing() on iOS11 has problems with largeTitles mode

We have found what seems to be a bug in UIKit but wanted to post here to see if anyone else has this problem or found a solution.
We're trying to use the new iOS11 large titles and hoisted search bar/refreshcontrol. We seemed to have found a problem where the root viewController of the navigation stack shows a minor display issue (problem A) but once another viewcontroller is pushed onto the navigation stack, the display goes nuts (problem B):
Things to note:
The problem is worse on the 2nd VC in the stack rather than the 1st
The refreshControl is not the green color the code sets it to the 1st time you see it on each sceen
The refreshControl slides down as you pull to refresh, it shouldn't do this
This odd behavior seems to only be a problem when we programmatically do a "pull to refresh" in viewDidLoad so that the user can see that the data is loading when they enter the screen. If we remove the lines that invoke refreshControl?.beginRefreshing() the display is clean. I've recreated this problem in a sample vanilla app. This is the entirety of the viewcontroller that shows the problem:
import UIKit
class ViewController: UITableViewController {
var tableHeaderSearchController: UISearchController!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.navigationController?.navigationBar.prefersLargeTitles = true
self.navigationController?.navigationItem.largeTitleDisplayMode = .always
tableHeaderSearchController = UISearchController(searchResultsController: UITableViewController())
navigationItem.searchController = tableHeaderSearchController
refreshControl?.tintColor = UIColor.green
refreshControl?.backgroundColor = UIColor.clear
refreshControl?.attributedTitle = NSAttributedString(string: "Loading Stuff...", attributes: [NSAttributedStringKey.font: UIFont.systemFont(ofSize: 17)])
refreshControl?.addTarget(self, action: #selector(refreshPulled), for: .valueChanged)
// Commenting out these 2 lines makes it work fine but you can't see the initial refresh spinner
refreshControl?.beginRefreshing()
refreshPulled()
}
#objc func refreshPulled() {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [refreshControl] in
refreshControl?.endRefreshing()
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Here's the storyboard. It's just a vanilla tableviewcontroller wrapped in a navigationController. 3 static cells, the 2nd one traverses to another instance of the same controller type.
Any ideas would be greatly appreciated. We'd really like to adopt the new look but this stuff is making it very hard to do so.
First, it is absolutely crucial that the table view extend up underneath the navigation bar and that is iOS 11 offset behavior be correct:
self.edgesForExtendedLayout = .all
self.tableView.contentInsetAdjustmentBehavior = .always
Second, scrolling to show the refresh control when you refresh manually is up to you, and calculating the amount is not at all simple:
self.refreshControl!.sizeToFit()
let top = self.tableView.adjustedContentInset.top
let y = self.refreshControl!.frame.maxY + top
self.tableView.setContentOffset(CGPoint(0, -y), animated:true)
self.refreshControl!.beginRefreshing()
The bar still stays too big during the refresh, but I don't see what can be done about that. Basically Apple has implemented large titles and shown the refresh control in the nav bar without thinking through the effects or dealing with the resulting bugs.

why pass viewController as variable?

First of all I have to say I am really new to swift and Objective C.I am learning them by myself.
I have a question for this code
I have a delegate in my SettingViewController called "settingsViewControllerFinished" and it pass the whole controller as a variable.
the code like this:
in my SettingViewController.swift
protocol SettingViewControllerDelegate: class {
func settingsViewControllerFinished(settingsViewController: SettingsViewController)
}
#IBAction func close(sender: AnyObject) {
dismissViewControllerAnimated(true, completion: nil)
self.delegate?.settingsViewControllerFinished(self)
}
I am confused.What did you mean if you pass the whole controller as a variable?(maybe the question is silly for you)
in my viewController:
func settingsViewControllerFinished(settingsViewController: SettingsViewController)
{
self.brushWidth = settingsViewController.brush
self.opacity = settingsViewController.opacity
self.red = settingsViewController.red
self.green = settingsViewController.green
self.blue = settingsViewController.blue
}
I guess the reason is:I pass everything in SettingViewController to ViewController so that I could use the variables in SettingViewController.
Am I rihgt?
Generally you are correct, yes: passing the SettingViewController back to its delegate enables the original caller to not have to keep a reference to the created and shown SettingViewController since the delegate method sends the relevant information along already.
But there is more: In some cases of delegates this style is useful for something different. Imagine a click handler consisting of a function func somethingGotClicked(sender:YourSenderType). If your class creates multiple instances of YourSenderType and shows them at the same time registering itself as their delegate there would be no way to know which one got clicked if there was no sender parameter. In some func somethingGotClicked() you would not know which one got clicked. That capability is often needed when showing multiple UITableView or UICollectionView is one single view with one single instances set as their delegate.

NSTextView, NSFontManager, and changeAttributes()

So I have this app that I'm writing to get familiar with Swift and programming for OSX. It's a note-taking app. The note window consists of an NSTextView and a button that brings up an NSFontPanel.
Changing the font works great. Selecting a size? No problem. Want to change attributes of the font like color, underlining, etc? I'm not at all sure how to get this to work.
Other sources (here and here, for example) seem to suggest that NSTextView should be the target of NSFontManager and that NSTextView has it's own implementation of changeAttributes(). Making NSTextView the target, however, does nothing. When I select text in NSTextView and bring up the font panel, the first selection I make in the fontPanel results in deselection of the text.
Making my view controller the target for NSFontManager and implementing a stub for changeAttributes yields an object of type NSFontEffectsBox which I am unable to find any good documentation for.
Question is... what am I supposed to do with NSFontEffectsBox? If in the fontPanel I select blue text with double underline, I can see those attributes in the debugger but I'm unable to access them programatically.
Here's the relevant code:
override func viewDidLoad() {
super.viewDidLoad()
loadNoteIntoInterface()
noteBody.keyDelegate = self // noteBody is the NSTextView
noteBody.delegate = self
noteBody.usesFontPanel = true
fontManager = NSFontManager.sharedFontManager()
fontManager!.target = self
}
Code for changing the font. This works just fine.
override func changeFont(sender: AnyObject?) {
let fm = sender as! NSFontManager
if noteBody.selectedRange().length>0 {
let theFont = fm.convertFont((noteBody.textStorage?.font)!)
noteBody.textStorage?.setAttributes([NSFontAttributeName: theFont], range: noteBody.selectedRange())
}
}
Stub code for changeAttributes:
func changeAttributes(sender: AnyObject) {
print(sender)
}
So.. my goals are two:
Understand what's going on here
have any changes I make in the fontPanel be reflected in the NSTextView selected text.
Thank you.
So I did manage to find an answer of sorts. Here's how I implemented changeAttributes() in my program:
func changeAttributes(sender: AnyObject) {
var newAttributes = sender.convertAttributes([String : AnyObject]())
newAttributes["NSForegroundColorAttributeName"] = newAttributes["NSColor"]
newAttributes["NSUnderlineStyleAttributeName"] = newAttributes["NSUnderline"]
newAttributes["NSStrikethroughStyleAttributeName"] = newAttributes["NSStrikethrough"]
newAttributes["NSUnderlineColorAttributeName"] = newAttributes["NSUnderlineColor"]
newAttributes["NSStrikethroughColorAttributeName"] = newAttributes["NSStrikethroughColor"]
print(newAttributes)
if noteBody.selectedRange().length>0 {
noteBody.textStorage?.addAttributes(newAttributes, range: noteBody.selectedRange())
}
}
Calling convertAttributes() on sender returns an attribute array, but the names don't seem to be what NSAttributedString is looking for. So I just copy them from old name to new and send them on. This is a good start, but I will probably remove the old keys before adding the attributes.
The question remains, tho.. is this the right way to do things?