Dynamic updating of label/button title in Swift - ios

I want to update the title property of a user interface element on iOS using Swift. In this case it is a UIBarButton, but it could be a UILabel, UIButton or whatever. Currently I am using this code, which works:
func setStatusMessage(barButton: UIBarButtonItem) {
let currentVersion = StatusModel.getCurrentVersion()
var statusUpdates = [StatusModel]()
var statusForCurrentVersion: StatusModel!
var statusMessage = String()
// check if update required before setting the text
checkIfLocalStatusNeedsUpdate()
barButton.title = getLocalStatusMessage()
// Try to update status anyway...
getStatusFromRemoteSource { (statusUpdates) -> Void in
for status in statusUpdates {
if status.version == currentVersion {
statusForCurrentVersion = status
}
}
self.saveStatusFromRemoteSource(statusForCurrentVersion)
barButton.title = statusForCurrentVersion.message
}
}
Although effective, this solution is ugly too as it does require a user interface (view) element to be embedded in my model. Not exactly a sort of MVC beauty.....
I cannot simply use return because the local status will be returned before the remote status can be fetched. So I guess I need some kind of handler / listener / delegate (/*getting lost here*/) to update the view dynamically. In this case that means: set the title using the locally stored value and update it if a remote value is received.
What is the best way to approach this scenario in a MVC compliant way, removing UI elements from the model code (thereby increasing reusability)?

One intermediate step you could do is going over the controller to the UI, by replacing the barButton parameter with a reference to the controller
barButton.title = statusForCurrentVersion.message
->
self.controller.update(title: statusForCurrentVersion.message)
This way your code is allowed to grow (updating more labels etc), but it comes with a cost of more code and harder readability.

Related

iOS - Sharing viewModel between views

I have a view whose ViewModel configures the view. The user can update the ViewModel and this object is later passed onto another view which will reflect the state of the preview view. Here is an example.
struct ViewSettings {
var btn1Selected: Bool
var btn2Selected: Bool
var btn3Selected: Bool
init() {
btn1Selected = true
btn2Selected = true
btn3Selected = true
}
}
class ViewOne: UIView {
var settings: ViewSettings
init(settings: ViewSettings) {
self.settings = settings
}
func configureView() {
btn1.isSelected = settings.btn1Selected
btn2.isSelected = settings.btn2Selected
btn3.isSelected = settings.btn3Selected
}
#objc func tapBtn1(_ sender: UIButton) {
btn1.isSelected = btn1.isSelected.toggle()
settings.btn1Selected.toggle()
}
#objc func tapBtn2(_ sender: UIButton) {
btn2.isSelected = btn2.isSelected.toggle()
settings.btn2Selected.toggle()
}
#objc func tapBtn3(_ sender: UIButton) {
btn3.isSelected = btn3.isSelected.toggle()
settings.btn3Selected.toggle()
}
}
This setting is later used inside another view. If btn1 is selected in ViewOne and when ViewTwo uses that setting, btn1 in ViewTwo is selected too.
Question:
I'm doing a direct mutation on the settings to achieve this. Is there a better design pattern that would let me arrive at the same solution?
your viewModel is struct that means its a value type, even when you think you are passing the same viewModel to other views, in reality you send a different copy of viewModel not the same instance.
So when you mutate value in view1 and pass the copy of it to view2, if view2 changes the value again, your viewModel in view1 will not be updated, its not passed by reference its passed by value.
so If your question was that I am mutating value directly will it cause side effects no because they are different copies, but if you want the changes to be reflected in all the views that holds this viewModel then it won't.
Finally answering your question
Question: I'm doing a direct mutation on the settings to achieve this.
Is there a better design pattern that would let me arrive at the same
solution?
At very first, sharing viewModel across view itself is arguable. Should this be done or not, as such the whole idea is opinion based. Some might say its fine some might say its not!
In general, I have seen people sharing viewModel across views but I personally refrain from doing so, but that doesn't mean that either of the approach is the only right way. Its best left to developers judgement.
Few clarifications:
struct ViewSettings {
var btn1Selected: Bool
var btn2Selected: Bool
var btn3Selected: Bool
init() {
btn1Selected = true
btn2Selected = true
btn3Selected = true
}
}
This looks more like DataModel and less of ViewModel isn't it? Its just a data container, no business logic, no data presentation/modification mechanisms nothing in it, its a plain data model. You are sharing DataModel across view and you wanna mutate and pass it on to next view I think its fine to go ahead with it.

Show or hide items by clicking button

I have four imageview contents in an XIB and a button that covers all my XIB. I want to make when the user tap the button, the first imageview is shown, the next tap is hidden and the second imageview is displayed and so on until all my imageview is shown / hidden. What would be the most efficient way to do it?
Save all your UIImageViews to an array, and current showing imageView to a variable, it may look like this:
var imageViews: [UIImageView] = []
var currentImageViewIndex = 0 {
didSet {
if currentImageViewIndex >= imageViews.count { currentImageViewIndex = 0 }
imageViews[oldValue].isHidden = true
imageViews[currentImageViewIndex].isHidden = false
}
}
func handleTap() {
currentImageViewIndex += 1
}
I suggest you use a state variable that contains an enum listing the various states (firstImageVisible, secondImage.... ) then you can have a function inside the enum that switches to the nextState (being the target of your button action) you can also easily iterate through states of an enum, check the documentation for the CaseIterable protocol. Often having a property observer (didSet) on the state is a handy place to update other parts of the UI which need to change every time the state changes.

Passing data between views via segmented bar

I know this is a frequently asked question throughout the forums and I hate to ask another seemingly simplistic question, but I can't manage to find a solution on data passing in regards to my specific circumstance.
Basically, I have a view controller embedded within a navigation controller; of which displays a segmented bar, acting as a 'profile selector'. After selection, I want a series of images on another view to be changed after different profiles are selected, but the data passing isn't seem to be working whatsoever. I'm unsure if delegate is required within my specific circumstance.
Essentially; I'd just love an example of how to pass a case value for a segmented bar so I would be able to perform a simple case statement like follows: (where the case values have been passed from the previous view controller)
#IBAction func chooseImage(sender: AnyObject) {
switch myPhotoSegment.selectedSegmentIndex {
//if first segment selected
case 0:
//stop image animation if currently animating
newImageView.stopAnimating()
//update displayed image
newImageView.image = UIImage(named: "1.jpg")
//if second segment selected
case 1:
//stop image animation if currently animating
newImageView.stopAnimating()
//update displayed image
newImageView.image = UIImage(named: "2.jpg")
//if third segment selected
case 2:
//stop image animation if currently animating
newImageView.stopAnimating()
//update displayed image
newImageView.image = UIImage(named: "3.jpg")
//if fourth segment selected
case 3:
//stop image animation if currently animating
newImageView.stopAnimating()
//update displayed image
newImageView.image = UIImage(named: "4.jpg")
//by default, no segment selected
default:
newImageView.image = nil
}
}
I know my problem is long and probably poorly explained, but any feedback would be greatly appreciated. I kind of struggle with the whole logic and understanding of passing data, so if you could break down the solution for me as simply as possible; that would be incredible.
To share data between those two classes they either need to know about each other or know about a shared object. For example, you could create a singleton data model that contains the properties you want to pass back and forth.
private let instance = MySingleton()
class MySingleton {
var somethingImInterestedIn: String?
var sharedInstance: MySingleton {
get {
return instance
}
}
}
Each class would get a reference to this by using:
MySingletion.sharedInstance
Once they have the reference to the singleton they can set or get any of the properties in that object. For your case you would want to store an enum instead of a String.

Running a segue after selecting a table once a Asynchronous request is finished

I'm trying to run a url request to get a JSON file after a certain table row is selected, based on the row a unique ID is sent with the URL request and a different JSON is generated. Here is my prepareforSegue
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
// Get the new view controller using segue.destinationViewController.
var divisionScene = segue.destinationViewController as! DivisionViewController
// Pass the selected object to the new view controller.
if let indexPath = tableView.indexPathForSelectedRow() {
let arrayIndex = indexPath.row
//println("Index: \(arrayIndex)")
torneoIDTransfer = torneos[arrayIndex].torneoID
//println("\(torneoIDTransfer)")
//check second url with second request type same token
//sets url to string using token
let tercerURL = NSURL(string: "http://api.zione.mx/get.json.asp?tr=3&tkn=\(tkn)&tor=\(torneoIDTransfer)")
//initializes request
let request = NSURLRequest(URL: tercerURL!)
NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.currentQueue()) { response, jsonDataRequest3, error in
let dataRequest3 = jsonDataRequest3
//takes data, saves it as json
let tercerJSON = JSON(data: jsonDataRequest3)
//checks to see that contents != nil, meaning the JSON file was found
if tercerJSON != nil {
//checks amount of tournaments available in order to generate table.
let divisionCount = tercerJSON["lista-divisiones"].count
//sets global variable numero de torneos
numeroDeDivisiones = divisionCount
//for loop to go insert into Torneo nuevo each ID and name by using count from above
for var index = 0; index < divisionCount; ++index {
var divisionID = Int(tercerJSON["lista-divisiones" ][index]["DivisionID"].number!)
var nomDivision = tercerJSON["lista-divisiones"][index]["nomDivision"].string
//println("\(divisionID)")
//println("\(nomDivision)")
var divisionNuevo = listaDivisiones(divisionID: divisionID, nomDivision: nomDivision!)
divisiones.append(divisionNuevo)
numeroDeDivisiones = 10
print("WHO IS FIRST")
}
}
print("\(divisiones[0].nomDivision)")
}
}
}
And I created my segway by dragging from the table cell to the new view Controller. However when I click the table cell the transition occurs instantly, before the request has a chance to finish and as a result no data is displayed.
It would be ideal to fetch and process the data in the background, long before the user ever selects a table row. If this is not possible, then I would suggest having your destination view controller do the URL request. The URL request happens asynchronously, so it will never have a chance to finish before your source view controller is deallocated.
In your source view controller, modify prepareForSegue
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
var divisionScene = segue.destinationViewController as! DivisionViewController
if let indexPath = tableView.indexPathForSelectedRow() {
let arrayIndex = indexPath.row
torneoIDTransfer = torneos[arrayIndex].torneoID
let tercerURL = NSURL(string: "http://api.zione.mx/get.json.asp?tr=3&tkn=\(tkn)&tor=\(torneoIDTransfer)")
divisionScene.fetchDataAtURL(tercerURL)
}
}
You'll need to define a method inside your destination view controller to handle the fetching.
func fetchDataAtURL(URL: NSURL) {
let request = NSURLRequest(URL: tercerURL!)
NSURLConnection.sendAsynchronousRequest( // ... your fetching logic here
}
You'll also need some logic to display the data once it arrives. I would suggest putting it into your request's completion callback (or rather, having the callback trigger a display update). If your divisionScene is a tableView, you might be able to call reloadData (after you update the data source). If not, you'll need some other way to update the UI. If you are updating the UI, make sure to dispatch to the main queue for that part.
Doing this way at least passes the URL loading responsibility to the destination view controller, which will at least be around when the data finally gets there.
" the transition occurs instantly, before the request has a chance to finish".
Of course it does, that is exactly what asynchronous means. You make a point of mentioning it is asynchronous but you must have a misunderstanding about what that means. Explain what you think it means and what you expected your code to so so that you can be better educated by us.
When you call sendAsynchronousRequest() think of your program as branching into two (actually that is what does effectively happen). One part is your original code which will continue to execute i.e your prepareFoSegue code will continue to execute.
Meanwhile, in parallel, the OS will execute the request, and when the request has finished the code in the block that you passed to sendAsynchronousRequest() will be executed. Therefore your prepareForSeque() function will finish before the Json has been received.
But apart from all that, you should not be attempting or hoping or wanting the JSon to be fetched before the segue transition - to do this would halt your ui. Suppose sendAsynchronousRequest() was instead sendSynchronousRequest() and it took 10 seconds to complete, what do you think the consequence would be on your app when it runs?
You should either fetch your data a long time before you GUI is ready to display it, or if that is not possible, display your GUI immediately with no data and then update it as the data arrives.

MvvmCross and UICollectionView how to bind SelectedItem from VM to View

I'm using MvvmCross with UICollectionView.
Bindings work perfectly, I have all my data properly displayed, and even if I select an item in CollectionView it gets properly set in my ViewModel.
For SelectedItem I use the following binding:
set.Bind(_collectionViewSource).For(x => x.SelectedItem).To(vm => vm.SelectedMachine);
The only problem I have is that I want a first CollectionViewItem to be selected initially.
As the sources of MvvmCross say that's not supported currently (in the setter for SelectedItem):
// note that we only expect this to be called from the control/Table
// we don't have any multi-select or any scroll into view functionality here
So, what's the best way to perform initial pre-selection of an item? What's the place I can call _collectionView.SelectItem from?
I tried calling it when collection changes, but that doesn't seem to work.
If you need this functionality, you should be able to inherit from MvxCollectionViewSource and to add a property something like
public event EventHandler SelectedItemExChanged;
public object SelectedItemEx
{
get { return base.SelectedItem; }
set
{
base.SelectedItem = value;
var index = FindIndexPath(value); // find the NSIndexPath of value in the collection
if (index != null)
_collectionView.SelectItem(index, true, UICollectionViewScrollPosition.CenteredHorizontally);
var handler = SelectedItemExChanged;
if (handler != null)
handler(this, EventArgs.Empty);
}
}
That can then be bound instead of SelectedItem
What's the place I can call _collectionView.SelectItem from? I tried calling it when collection changes, but that doesn't seem to work.
If that doesn't work, then I'm not sure - you are probably heading into animation timing problems - see questions like uicollectionview select an item immediately after reloaddata? - maybe try editing your question to post a bit more of your code - something that people can more easily hope with debugging.

Resources