Interact with element in UITableView.backgroundView - ios

I have a UITableView which displays some data. If there is no data to display, I show an empty with a button in it. I'm taking advantage of the tableView's backgroundView feature to display my empty state. The problem is that I cannot interact with that button. I assume the tableView disables interaction with the background view. Is there some way to enable it?

You should create strong variable to your background view controller.
class ExampleController: UIViewController {
...
#IBOutlet weak var tableView: UITableView!
private let emptyStateVC = EmptyStateVc(nibName: "EmptyStateVc", bundle: nil)
...
private func setupTableView() {
tableView.backgroundView = emptyStateVC.view
tableView.backgroundView?.isUserInteractionEnabled = true
}
}

Related

Using UIColorPickerViewController to change the color of cell background color always returns black

I'm developing an app for my school that helps students better understand their grades by getting an analysis on their portfolio of assignments for each class. Right now I am at the point of letting the user create the classes they are in and customize the information within it that is displayed in a tableview with custom cells. The user gives the majority of the information in a child view where they input information such as class name, teacher, grade weighting, etc. I wanted to give the user the ability to change the color of the cell once they are viewing the TableView with all the cells - classes - they made. I decided to go about this by having the cells have a UIButton that they can click on for their determined cell to then pull up a UIColorPickerViewController.
What I wanted to happen was...
User taps button in cell
UIPickerViewController is pulled up
User selects their desired color
User exits UIPickerViewController
Cell is changed to the color
What is actually happening is this
User taps button in cell
Cell background becomes black right as UIPickerViewController is presented
User selects their desired color
User exits UIPickerViewController
Cell remains black
I used a delegate to send the information from the cells and then I used the "colorPickerViewControllerDidFinish()" function and it's still not working out. When I did some debugging I found that the value of the UIColorPickerViewController is actually being stored in the variable I am using, but only after I have already assigned it's value to the cell background so I'm unsure what to do. As you can probably tell, I'm new to swift so apologies for any stupid mistakes in my code.
Custom Cell File
// Protocol for VC's to conform to so they can distinguish which cell has a button being tapped
protocol newlyCreatedCellDelegate: AnyObject
{
func didTapButton(title: String, cellView: UIView)
}
class newlyCreatedClass: UITableViewCell {
// Telling the delegate what to do once they are assigned
weak var delegate: newlyCreatedCellDelegate?
#IBOutlet weak var classContentView: UIView!
#IBOutlet weak var classUIView: UIView!
#IBOutlet weak var classNameLabel: UILabel!
#IBOutlet weak var classTeacherNameLabel: UILabel!
#IBOutlet weak var pointType1NameLabel: UILabel!
#IBOutlet weak var pointType2NameLabel: UILabel!
#IBOutlet weak var pointType3NameLabel: UILabel!
#IBOutlet weak var percent1Label: UILabel!
#IBOutlet weak var percent2Label: UILabel!
#IBOutlet weak var percent3Label: UILabel!
#IBOutlet weak var colorButton: UIButton!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
colorButton.layer.cornerRadius = 21
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
// Essentially creating the prep list for the delegate. If they are called - whoever it is - they will go through this 'checklist'
#IBAction func colorButtonTapped(_ sender: Any)
{
delegate?.didTapButton(title: classNameLabel.text!, cellView: classUIView)
}
}
ViewController Extensions
extension ClassSelection: newlyCreatedCellDelegate
{
func didTapButton(title: String, cellView: UIView)
{
let colorPickerVC = UIColorPickerViewController()
colorPickerVC.delegate = self
present(colorPickerVC, animated: true, completion: nil)
colorPickerViewControllerDidFinish(colorPickerVC)
// 'cellBackgroundColor' is a variable declared in the VC to transfer the UIColor value
cellView.backgroundColor = cellBackgroundColor
}
}
extension ClassSelection: UIColorPickerViewControllerDelegate
{
func colorPickerViewControllerDidFinish(_ viewController: UIColorPickerViewController) {
cellBackgroundColor = viewController.selectedColor
}
}
You should implement one more UIColorPickerViewControllerDelegate method:
func colorPickerViewControllerDidSelectColor(_ viewController: UIColorPickerViewController) {
cellBackgroundColor = viewController.selectedColor
}
It's a great start! As a really direct answer to your original question:
The reason for your problem is that in your didTapButton function you are presenting the color picker, but then immediately telling the app that the user is done with the picker, and then immediately setting the background color to cellBackgroundColor, which I assume has a default value of UIColor.black.
Instead you should delete those last 2 lines in your didTapButton function - just initialize the picker, set the delegate, and present the picker. Then the delegate method you chose - colorPickerViewControllerDidFinish isn't really the correct one for your purpose. Instead consider using the didSelect delegate method (see docs). When that method is called it will pass you the color the user selected, which you can simply use to set your background color and refresh your tableView cell if needed.
Since you mention you are a new Swift dev I will also mention that UITableView reuses its cells, so simply setting the background color of a cell once will not have the result you are expecting. You will see that as you scroll the cells up and down the colors will change in the various cells, so ultimately you'll need to store the color selections in another way so that each time a cell is being dequeued you can set the correct color based on user input. That part is outside of the scope of the original question, but just letting you know.

ViewController or UIView should be in control?

So I have a 'ListViewController' that has a subview called 'ListView'
ListView is the master view that holds all remaining subviews.
Inside listView, there is a subview called PhotoView and inside it, it has a UIButton that toggles the constraints of PhotoView, resizing based on arbitrary values.
Hierarchy is as follows:
ListViewController <- ListView <- PhotoView
The reason I've done this is to minimize the amount of code inside each view, to segment it out.
My question is: Who should be in charge of the 'resizing' function? My
understanding is that a viewController should generally handle the
main functions that are inside it pertaining to its subviews.
However in this case, it is being handled inside its subviews subview, 'PhotoView'. Should I use protocols to persist it to the ListViewController? PhotoView(Protocol) -> ListView(Protocol) -> ListViewController?
Is this a correct method? Or is there a more efficient way of handling subview functions?
Thanks guys
If you move all logics into ListViewController, everything is very simple as you can assign any UIControl in the VC:
class ListViewController : UIViewController{
#IBOutlet var goButton: UIButton! //In the DetailView
#IBOutlet var photoView: PhotoView!
#IBOutlet var detailView: DetailView!
// MARK: --
#IBAction func testCommand(_ sender: Any){
PhotoView.backgroundColor = UIColor.red
detailView.backgroundColor = UIColor.red
}
// MARK: --
#IBAction func photoViewCommand(_ sender: Any){
PhotoView.backgroundColor = UIColor.red
detailView.backgroundColor = UIColor.red
}
// MARK: --
#IBAction func detailViewCommand(_ sender: Any){
PhotoView.backgroundColor = UIColor.red
detailView.backgroundColor = UIColor.red
}
}
You don't need to even know the ListView. Use IB can assign all UIViews , not only direct children, but all subview trees, including UIControls, like buttons and labels , as long as they lies in the scene of same UIViewController and their methods is in the same VC not in other separated views.
If you have the code of views, you can move it to the VC easily. Just one class to control all. If you need to know who is who. Adding Mark is enough for small files.

iOS - How to init and insert UICollectionViewCell programmatically when data is ready?

I would like to add Ads in my App, like Native Ads or Parallax.
The App is mainly an UICollectionView with many cells displaying different information.
I am using an SDK in order to load the Ad Object.
I have made a custom UICollectionViewCell class in order to init the cell and load the Native Ad as a cell in my UICollectionView.
#objc class AdsCollectionViewCell: BaseCollectionViewCell, AdapterAdsDelegate {
#IBOutlet var outletNativeTitle: UILabel!
#IBOutlet var outletImageNative: UIImageView!
#IBOutlet var outletButtonAction: UIButton!
#IBOutlet var clickableView: UIView!
#IBOutlet var mediaContainer: UIView!
#IBOutlet var bottomView: UIView!
var adFactory = AdsSDKFactory()
var adObject = AdsObject()
#objc func initCell(viewController: HomeViewController) {
DispatchQueue.main.async {
self.isHidden = true
}
// Set some properties for the Ads SDK
adFactory.nativeDelegate = self
adObject.viewController = viewController
adFactory.placementId = "154569"
// method from the SDK to request the Ad
adFactory.loadAds()
}
It is working but my main issue is that I am displaying an empty cell and the content is displayed when I got the delegate response method from the Ad SDK with the Ad Object.
Here is the delegate method I receive in my AdsCollectionViewCell.
func adObjectDidLoad(adView: AdsObject!) {
self.adObject = adView
self.updateCellOutlets() // my method to update labels from the adView
self.setNeedsDisplay()
self.delegate.tileCellDidSucceed!(self)
}
I would like to first init and make the call to the adFactory.loadAds in my HomeViewController, so that when I got the delegate response and the adObject, I don't updateCellOutlets directly but I can self.delegate.tileCellDidSucceed!(self) which is the delegate from my BaseCollectionViewCell in order to notify my HomeViewController that the cell is ready.
How can init a cell outside of the cellForRow method to dynamically insert it when the Ad is loaded ?
EDIT: I have followed the advices and create a Singleton AdsLoader class, that set all the parameters to request the ad, and also make the load request.
I set my HomeViewController as delegate so that I can get the view ad as response directly in my HomeViewController.
My issue now is to insertObject in the collectionView at the right indexPath. Which object should I insert?
I am unable to get a clear picture of the code and specific requirements of your AdSDK. So i will go ahead and write a pseudo code as per my understanding of what needs to be done. Here is what I purpose, instead of calling adFactory.loadAds() into the UICollectionViewCell object, you can perform this task into a different class object. Let say AdLoader
So your add loader should be something like this.
class Adloader {
var adFactory = AdsSDKFactory()
typealias adLoadCompletionHandler = (adView: AdsObject) -> Void
var loadCompletion
func initLoader() {
//Set some properties for the Ads SDK
adFactory.nativeDelegate = self
// method from the SDK to request the Ad
adFactory.loadAds()
}
}
Once the ad is loaded, you can use a completion handler to get update the collection view.
func adObjectDidLoad(adView: AdsObject!) {
adLoadCompletionHandler(adView)
}
So your code can be something like this.
if (needToInsertAd) {
var newAd = Adloader.initLoader()
newAd.adLoadCompletionHandler {
//Code to insert cell at indexpath
}
}
Hope this helps you.

I want my view controllers to have the same combination of custom UIViews which communicate with each other

I want my several view controllers to have a player in the bottom. This player consists of 2 views: the player and a button which toggles it (can be hidden or expanded).
Now I use the code below in each view controller to add this player.
#IBOutlet weak var broadcastView: BroadcastView!
#IBOutlet weak var broadcastViewBottomConstraint: NSLayoutConstraint!
#IBOutlet weak var avatarImageView: UIImageView!
#IBAction func toggleBroadcastMode(_ sender: ToggleBroadcastButton) {
if sender.isExpanded {
broadcastViewBottomConstraint.hideBroadcastView()
} else {
broadcastViewBottomConstraint.expandBroadcastView()
}
animateBroadcastToggle()
sender.toggle()
broadcastView.toggleBroadcastView()
}
Is there a way not to duplicate the code over and over? Maybe I can create parent VC or View to do it? If so, then how?
I personally would subclass a UINavigationController and have it in there, that way you can navigate through the flow while the player stays looking good at the bottom, if you need a VC to interact with it then you can
if let nav = navigationController as? MyPlayerNavController {
nav.PlayThis()
}
you can have it change size and everything from there and you wont lose it during transitions and stuff like the music app when playing music.
Add it as a subview of the keyWindow. That way it will always stay in all the viewControllers.
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
var overlayViewFrame = UIScreen.main.bounds
overlayViewFrame.origin.y = overlayViewFrame.height - 200
overlayViewFrame.size.height = 200
let overlayView = UIView(frame:overlayViewFrame)
overlayView.backgroundColor = UIColor.cyan
UIApplication.shared.keyWindow?.addSubview(overlayView)
}
This code is a sample generic code. If you want certain VCs to not show this, keep this overlay view in a singleton, and hide in appropriate VCs.
Output screenshots:

Custom container: #IBOutlets nil even though view is displaying correctly

I have created a UIViewController. Within this view controller I have a container view.
In the code I instantiate the container view like so:
var answerButtons: AnswerButtonsViewController!
override func viewDidLoad() {
super.viewDidLoad()
answerButtons = AnswerButtonsViewController()
answerButtons.delegate = self
println(answerButtons.name) // This prints out the correct string
println(answerButtons.answerOne) // This prints out nil
println(answerButtons.buttons) // This prints out an empty array
retrieveSelectedCategory(selectedCategory!)
}
However the container view is definitely instantiate because within the container view class I set the background color of the UIButton's and this displays correctly within my app.
Here is the code from my container view:
#IBOutlet var answerOne: UIButton!
#IBOutlet var answerTwo: UIButton!
#IBOutlet var answerThree: UIButton!
#IBOutlet var answerFour: UIButton!
#IBOutlet var answerFive: UIButton!
#IBOutlet var answerSix: UIButton!
#IBOutlet var answerSeven: UIButton!
#IBOutlet var answerEight: UIButton!
var buttons: Array<UIButton> = []
var name = "Is this set"
var delegate: AnswerButtonsViewControllerDelegate?
override func viewWillAppear(animated: Bool) {
addAnswerButtonsToArray()
buttons.map(customiseButtons)
}
func addAnswerButtonsToArray() {
buttons.append(answerOne)
buttons.append(answerTwo)
buttons.append(answerThree)
buttons.append(answerFour)
buttons.append(answerFive)
buttons.append(answerSix)
buttons.append(answerSeven)
buttons.append(answerEight)
}
func customiseButtons(button: UIButton) {
button.layer.cornerRadius = 5
button.setTitleColor(UIColor.blackColor(), forState: .Normal)
button.backgroundColor = GSNColor.veryLightGrayColor()
var title: String! = button.titleLabel?.text
title.replaceRange(title.startIndex...title.startIndex, with: String(title[title.startIndex]).capitalizedString)
button.titleLabel?.text = title
}
#IBAction func answerButtonPressed(sender: UIButton) {
delegate?.answerButtonPressed(sender)
}
Also the IBAction of the buttons work.
So I can't understand why my property answerButtons is initiated and the name property is set ok. However the properties are nil when printed and the array empty. Yet the view is being displayed correctly.
Any insights would be much appreciated and if you need any more details please let me know.
Edit
Here is a picture of storyboard with the connections:
The controller embedded in the container view is instantiated at the same time that the controller with the container view is. You should not instantiate it yourself. You only need to get a reference to it. A controller in a container view is a child view controller of the controller whose subview is the container view. Instead of this,
answerButtons = AnswerButtonsViewController() // this creates another instance that you never use, and is never on screen
You should have this,
answerButtons = self.childViewControllers[0] as AnswerButtonsViewController
Here, you are just instantiating the view controller.You only get a reference to it.You have to instantiate the view controller using storyboard
let SB: UIStoryboard = UIStoryboard(name: StoryboardName, bundle: Bundle.main)
let VC = SB.instantiateViewController(withIdentifier: StoryboardIdOfViewController)
if let _ = VC.view {
//Do here
}
The IBOutlet's will be nil until the view is loaded. All you are doing is instantiating the container view controller, not displaying or loading the view.
The IBOutlet's won't be available until the AnswerViewController's loadView and viewDidLoad method's are called.
You can check:
if (answerButtons.isViewLoaded()) {
}
To see if the view is loaded. If you want the view to be loaded you'll either need to access the view property of the controller, or add the view to the view hierarchy as a subview.

Resources