I'd like to use RxSwift in my project, but because I'm newbie I maybe misunderstand some principles. Its clear for me how to catch button presses or use rxswift with UITableView with dynamic cells (there are a lot of tutorials for that), but I don't understand how to use it with UITableView with STATIC cells - I'd like to develop something like iOS Settings.app. Could you show me example? Is it a good practice to use rxswift for it? Or maybe I should use something else?
You can drag a #IBOutlet weak var button: UIButton! from static table view cell button, So it's something like this:
class TableViewController: UITableViewController {
#IBOutlet weak var button: UIButton!
...
override func viewDidLoad() {
super.viewDidLoad()
...
button.rx.tap
.subscribe()
.disposed(by: disposeBag)
}
...
}
Hope this may help.
To handle static cells, you have to use a UITableViewController (which I assume you know) but you can still use the rx operators on its tableView.
Of course you don't need to use the items operator on it because the cells are already built, but you can still use itemSelected to determine which cell was tapped on:
final class ViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.rx.itemSelected
.subscribe(onNext: { print("selected index path \($0)") })
.disposed(by: bag)
}
let bag = DisposeBag()
}
Related
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.
I am using MVVM pattern with RxSwift, RxCocoa, RxDataSources.
I have successfully populated the UITableView with array of PaletteViewModel present in ListViewModel by using RxDataSource but it's one way binding.
I want to achieve what I have shown in the picture i.e. I want to bind the UITextField from UITableViewCell to the Observable which is present at some index in the array in ListViewModel
I want to do 2 way binding with the UITextField and answer property of the PaletteViewModel. If the user changes the text in the textField it should change the value in the answer property present at particular index and vice versa.
How Can I achieve something complex like this using MVVM pattern using ReactiveX frameworks?
What if the UITableViewCell at some IndexPath is removed from the memory as it's not visible and the observable's value is changed will it result in crash as the UITextField at that IndexPath will return nil?
A UITextField is an input element. You don't need a two way binding to it because you shouldn't be dynamically changing it. The most you should do is initialize it and you don't need a binding for that.
You don't mention what the final output will be for this input so the answer may be different than the below. This particular solution assumes that you need to push all the answers as a group to a server or database. Maybe when a button is tapped.
There is a lot of code below, but it compiles as it stands (with the proper imports.) You can subscribe to ListViewModel.answers to see all the answers collected together.
class ViewController: UIViewController {
#IBOutlet weak var myTableView: UITableView!
let bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
let answersSubject = PublishSubject<(PaletteID, String)>()
let viewModel = ListViewModel(answersIn: answersSubject.asObservable())
viewModel.paletteViewModels
.bind(to: myTableView.rx.items(cellIdentifier: "Cell", cellType: MyCell.self)) { index, element, cell in
cell.answerTextField.text = element.initialAnswer
cell.answerTextField.rx.text.orEmpty
.map { (element.id, $0) }
.bind(to: answersSubject)
.disposed(by: cell.bag)
}
.disposed(by: bag)
}
}
class MyCell: UITableViewCell {
#IBOutlet weak var answerTextField: UITextField!
let bag = DisposeBag()
}
struct ListViewModel {
let paletteViewModels: Observable<[PaletteViewModel]>
let answers: Observable<[PaletteID: String]>
init(answersIn: Observable<(PaletteID, String)>) {
paletteViewModels = Observable.just([])
answers = answersIn
.scan(into: [PaletteID: String]()) { current, new in
current[new.0] = new.1
}
}
}
struct PaletteViewModel {
let id: PaletteID
let initialAnswer: String
}
struct PaletteID: RawRepresentable, Hashable {
let rawValue: String
}
maybe I am missing something really fundamental here, but after staring at the code for an hour or so, my brain is going trough cycles and I would appreciate a fresh glance at this problem.
I have the following UIView:
import UIKit
protocol DetailViewWillShowUpDelegate {
func sendDetailOpened(_ openedBool: Bool)
}
class locationXIBController: UIView {
#IBOutlet weak var loationLabel: UILabel!
#IBOutlet weak var vsedniOteviraciDobaLabel: UILabel!
#IBOutlet weak var prijmajiKartyLabel: UILabel!
#IBOutlet weak var detailViewButtonOutlet: UIButton!
#IBOutlet weak var backgroundViewButton: UIButton!
let openedBool = true
var detailViewWillShowUpDelegate: DetailViewWillShowUpDelegate?
override func awakeFromNib() {
super.awakeFromNib()
}
#IBAction func vecerkaDetailButtonPressed(_ sender: UIButton) {
detailViewWillShowUpDelegate?.sendDetailOpened(openedBool)
print("pressed")
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let result = detailViewButtonOutlet.hitTest(convert(point, to: detailViewButtonOutlet), with: event) {
return result
}
return backgroundViewButton.hitTest(convert(point, to: backgroundViewButton), with: event)
}
}
Now the problem is, that when I call/press the vecerkaDetailButtonPressed function I get "pressed" output in the console but the protocol for some reason doesn't go trough.
The other side looks like this (stripped for simplicity):
class MapViewController: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate {
let locationXIB = locationXIBController()
let isVecerkaDetailOpened = false
override func viewDidLoad() {
locationXIB.detailViewWillShowUpDelegate = self
}
extension MapViewController: DetailViewWillShowUpDelegate {
func sendDetailOpened(_ openedBool: Bool) {
isVecerkaDetailOpened = openedBool
print("success")
}
}
I know the protocol value at the moment of execution is nil. As I said, any help is appreciated, thanks!
First, a couple of naming convention issues:
The name locationXIBController is a bad choice for a UIView object. It is a view object, not a controller object.
Second, class names in Swift should start with an upper-case letter. So LocationXIBView would be a much better name for that view class.
Next, your code
let locationXIB = locationXIBController()
...is wrong. That creates a brand-new instance of your locationXIBController class that you never install in your view hierarchy. You should make that line an IBOutlet:
#IBOutlet weak var locationXIB: locationXIBController!
And then you should control-drag from the locationXIBController in your StoryBoard onto the outlet in your view controller. That will cause Interface Builder to connect the outlet.
Now when you run your program the variable locationXIB will be connected to the locationXIBController view from your storyboard/XIB when it's loaded.
In addition to the answer of #Duncan C, you might check whether you need super.viewDidLoad() at the top of the viewDidLoad() method in the MapViewController class? Not doing that can lead to quirky things in your app.
I asked:
So does detailViewWillShowUpDelegate actually point at anything, or is it nil?
And you replied:
I just tried debugging and it is actually nil
So that's the problem... you need to set detailViewWillShowUpDelegate to point to a valid delegate object. This is often done in the .xib file or storyboard, and sometimes people forget to make that connection, so check there if it makes sense. Else you'll just need to get a reference to the delegate at some point before the code in question can run and set it up.
Answer to the credit of #Paulw11
I finally managed to get it working by communicating like so:
step 1) 1:1 communication via protocol between MKAnnotation and MKAnnotationView
step 2) 1:1 communication via protocol between MKAnnotationView and MapViewController passing the same data
Finally works like a charm, thanks!
I am wondering what is the best approach to add an Activity Indicator to the bottom of the UICollectionView.
I would like to able to show and display this cell, and I am wondering how should I do this.
I have searched SO and I have found people suggesting adding a new cell to the last row. But I would like to have the instance of the activity indicator so that I can turn it on or off.
Any suggestions?
You can add a new cell to the last row of your collection view and create a custom UICollectionViewCell class for that cell where you can enter the logic of your cell e.g.:
import UIKit
class LoadingCollectionViewCell: UICollectionViewCell {
#IBOutlet weak var activityIndicator: UIActivityIndicatorView!
override func awakeFromNib() {
self.activityIndicator.startAnimating()
}
func stopLoading() {
self.activityIndicator.stopAnimating()
}
}
You can then call the function stopLoading() in your ViewController when you want the activity indicator to stop.
Maybe it is the wrong idea completely to use the same xib multiple times in one viewcontroller, but it looked in some way better than creating an x amount of views with the same labels and the same buttons..
And of course some fun with xibs:)
To give you a quick impression of what I achieved so far:
In the class belonging to the xib, I created a delegate so I could catch a button press in my main view controller.
import UIKit
protocol TimerViewDelegate : class {
func timerButtonTapped(buttonState : NSInteger)
}
class TimerView: UIView {
var delegate : TimerViewDelegate?
var currentButtonState = 0
#IBOutlet var view: UIView!
#IBOutlet var timerLabel: UILabel!
#IBOutlet var button: UIButton!
required init(coder aDecoder: NSCoder){
super.init(coder: aDecoder)
NSBundle.mainBundle().loadNibNamed("TimerView", owner: self, options: nil)
self.addSubview(self.view)
}
#IBAction func buttonTapped(sender:UIButton) {
if currentButtonState == 0{
currentButtonState = 1
} else{
currentButtonState = 0
}
self.delegate?.timerButtonTapped(currentButtonState)
}
}
I know it is not super fancy stuff, but at the moment I'm only evaluating if its any use to do this at all.
In my main view controller I registered outlets for the xibs in a way like:
#IBOutlet weak var timerView1 : TimerView!
#IBOutlet weak var timerView2 : TimerView!
#IBOutlet ...
And in viewDidLoad():
self.timerView1.delegate = self
self.timerView2.delegate = self
self...
Later I can catch the button presses in the following method:
func timerButtonTapped(buttonState: NSInteger) {
println("button press from nib, with state \(buttonState)")
}
From here it does make a difference if I press the button from the top xib or another one, since they keep track of their own buttonstate.
But how can I distinguish the different xibs from each other like this?
I can give the xibs themselves a tag, but I don't know if that has any use. Also talking to their labels from my main view, will have a simular problem..
Even if this is a completely wrong approach of using xibs, I'm still interested how to solve this.
Thank you for your time!
You pretty much have your solution already, you just need to improve your protocol method specification, basically by adding the TimerView that is passing on the button press.
(compare to a delegate protocol like UITableViewDelegate, where the table view always passes itself...)
So, something like:
protocol TimerViewDelegate : class {
func timerButtonTapped(sender: TimerView, buttonState : NSInteger)
}
and then the delegate can find out which TimerView is associated and do something with it.
Incidentally, it's likely best to store the TimerView instances in an array, sorted in some way, so that you can easily access the details.
You can use the tag property safely. Apple documentation says:
An integer that you can use to identify view objects in your application.