3

I am doing a complex application that has to do stuff on a lot of threads and frequently update the interface.

So I have to add a lot of

  dispatch_async(dispatch_get_main_queue(), ^{

  });

in the middle of the code to dispatch UI updates to the main thread and I find it ugly and disruptive as hell.

So, I have this idea of creating subclasses of elements like UILabel, UITextField, etc., overriding their main thread methods like this:

- (void)setAttributedText:(NSAttributedString *)attributedText {
  dispatch_async(dispatch_get_main_queue(), ^{
    [super setAttributedText:attributedText];
  });
}

- (void)setText:(NSString *)text {
  dispatch_async(dispatch_get_main_queue(), ^{
    [super setText:text];
  });
}

- (void)scrollRangeToVisible:(NSRange)range {
  dispatch_async(dispatch_get_main_queue(), ^{
    [super scrollRangeToVisible:range];
  });
}

but again, I have to have this same code scatter all over these classes.

Is there a better way?

Duck
  • 34,902
  • 47
  • 248
  • 470
  • This is going to be relatively expensive if you've got a few UI calls to make in sequence as each one of them is going to create a new block and dispatch to the main queue. Since the dispatches are async, too, one block can be evaluated immediately while others wait on something else, causing your UI to update inconsistently. I'd suggest finding a way to make the dispatch as non-ugly and non-intrusive as possible instead — you can make the code nicer with a macro or a helper function, or find a higher-level abstraction. – Itai Ferber Nov 26 '17 at 19:26
  • As you seem to use Obj-C could CPP macro('s) help our? – meaning-matters Nov 26 '17 at 19:39
  • @meaning-matters - can you give an example on how this is not be as pollution in the code than the mess I already have? – Duck Nov 26 '17 at 19:46
  • 1
    It would probably be simpler to use setters on data properties that can asynchronously dispatch updates to the relevant UI elements and then you can simply update the data model properties whenever you want and know that the UI will be updated eventually. You could also look for a binding framework to make this easier. – Paulw11 Nov 26 '17 at 19:47
  • @Paulw11 - sorry but your explanation sounds Klingon to me... I need code to see this... thanks – Duck Nov 26 '17 at 19:48
  • You can create your own setter function for a property, say `currentFoo`. In the setter function you can have a `dispatch_async` that sets `self.currentFooLabel.text = currentFoo`. Then in your background thread code at any time you can just say `self.currentFoo = @“newFoo”` or whatever and the setter function will take care of the dispatch async for you. – Paulw11 Nov 26 '17 at 19:51
  • Your example implementation will call itself recursively or am I missing something? I think each function must check for the mainthread and either call super's original implementation or schedule a block. – mschmidt Nov 26 '17 at 19:52
  • No, there is a difference between the property `currentFoo` and the text field you want to put that value into `currentFooLabel`. See [here](https://stackoverflow.com/questions/10425827/please-explain-getter-and-setters-in-objective-c) for information on objective c setters and getters – Paulw11 Nov 26 '17 at 19:55
  • @Paulw11 - sorry, my mistake. I have fixed it by replacing `self` with `super` but this method of mine will not work for the getter `-(NSString *)text` that must return the text inside the textview... damn! – Duck Nov 26 '17 at 20:00
  • That shouldn't be a problem. You can simply write the getter that returns the text from the corresponding text view. – Paulw11 Nov 26 '17 at 20:04
  • _"do stuff on a lot of threads"_ Your component that does stuff in the background should return its results on the main thread; it should not be the responsibility of its clients to sanitize the response. If you're using someone else's component that's badly behaved in this regard, then write a wrapper/adapter that does what you need. In other words, centralize the return to the main thread by whatever means necessary. – jscs Nov 26 '17 at 20:24
  • As @JoshCaswell said it is responsability of your background code/class to switch back to the main thread in case of need. I don't think it is a good practice to use setters of subclasses (as you want to to) to always force defensively the execution of your code on the main thread. – crom87 Nov 26 '17 at 20:49
  • 1
    @JoshCaswell I have to disagree. If client code makes a call to an asynchronous API, the callback should also be done on a background queue. The client should have the responsibility to decide whether it needs to perform certain actions on a specific queue (such as the main queue for UI updates). Think of the case where I call an async API from a background queue to begin with. Why should the completion callback be sent on the main queue by async API? Apple's frameworks are full of examples where completion blocks are not automatically called on the main queue. – rmaddy Nov 26 '17 at 21:22
  • Would you be willing to tell about the structure of your code and the ways background and main thread work is intertwined? By taking a step back and getting a broader view on your challenge, I/we may be able to suggest a better method. – meaning-matters Nov 27 '17 at 09:27

1 Answers1

1

I think your problem is arising because you are tightly coupling your computation to your UI. You can look at alternate architectures, such as MVVM, although formally adopting this architecture works best when you have a binding framework.

Even without a binding framework you can improve your situation by introduction some model properties with some setter/getter code to work with the actual UI element.

For example, declare a pair of properties, one for the text field and one for the text

@property (weak, nonatomic)  UITextField *someTextField;
@property (strong, nonatomic) NSString *someText;

Now, implement setter and getter methods to couple them:

-(NSString *)someText {
   return self.someTextField.text;
} 

-(Void *)setSomeText: (NSString *)newValue {
    dispatch_async(dispatch_get_main_queue(), ^{
        self.someTextField.text = newValue
  });
}

Now, this is a pretty simple example, and has a slight issue in that immediately retrieving the value of someText after setting it won't return the value that was just set due to the asynchronous nature of the set operation, but that shouldn't be an issue in most cases. It does demonstrate how you could start to decouple the data model from the UI elements and so separate concerns with data from concerns with the mechanics of updating UI elements.

Paulw11
  • 108,386
  • 14
  • 159
  • 186
  • ok, this is what I thought, but in my case I have to retrieve the value of the elements in the main thread, or the whole thing crashes... anyway, because this is the best answer so far, I accept that. – Duck Nov 26 '17 at 22:11
  • 1
    You shouldn't be grabbing the UI element data at random times from a background thread. You should get all of the data you need and pass it to your method before the code begins working on the background thread. – Paulw11 Nov 26 '17 at 22:12
  • no, I have the other way around. Everything performs on a background thread, then I need to update the interface. The background thread is the one that calculates things that will go to the interface. – Duck Nov 26 '17 at 22:14
  • Right, so then the code I have handles that. You can just set `self.someText` from the background thread and know that the text field will be updated in due course. – Paulw11 Nov 26 '17 at 22:15
  • app crashes when I do that and xcode shows an error telling me that self.sometext should be read from the main thread. – Duck Nov 26 '17 at 22:16
  • So, you are reading the UI elements from the background thread; you are saying something like `NSString *localVar = self.someText`. This is what I am saying you shouldn't do. You should get `localVar` before you begin your background operation and then pass it to the background code; `[myObject someBackgroundOperationWithText: localVar]` – Paulw11 Nov 26 '17 at 22:18
  • you are not getting the point. The value to update the interface comes from a background thread. There is no way to update it from the main thread. Believe me. – Duck Nov 26 '17 at 22:43
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/159862/discussion-between-paulw11-and-spacedog). – Paulw11 Nov 26 '17 at 22:44