*UPDATE***
My first description (now deleted) was'nt the real situation. It is a simplified version of the problem.
The Complete situation:
I want to change the UIImage within a UIImageView with a picture which I select from the iphone photo gallery.
For this I used a MediaPicker (part of the Xamarin library).
When I click a button, the Mediapicker will be created. Then I call a function to take a picture from the photo gallery. This methods expects 2 arguments. The Mediapicker and a callback function. --> PictureFromGallery(mediaPicker, CallbackPhotoMade);
This callback function will be trigger after a user selected a picture in the photo library to upload.
Within this callback function I want to change the UIImage of the UIImageView.
void CallbackPhotoMade(MediaFile obj)
{
imageviewPhoto1.Image = UIImage.FromFile("Images/image2.PNG");
//To test I just use a file from a folder in my project
}
When I breakpoint the above function (CallbackPhotoMade) and I put my mouse at the text ".Image =", the following message apears:
MonoTouch.UIKit.UIKitThreadAccessException: UIKit Consistency error: you are calling an UIKit method that can only be invoked from the UI thread
I think this is the problem why the UIImage within the UIImageView doesn't change.
Does anyone knows how to solve this?
*UPDATE2***
I read in another topic that this could be solved by setting CheckForIllegalCrossThreadCalls to false like:
public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
UIApplication.CheckForIllegalCrossThreadCalls = false;
}
Unfortunately the image still doesn't change.
Although, the specific error dissapeared.
Try this one:
void CallbackPhotoMade(MediaFile obj)
{
InvokeOnMainThread(() => {
imageviewPhoto1.Image = UIImage.FromFile("Images/image2.PNG");
});
//To test I just use a file from a folder in my project
}
The setting you made, just disables checking, but does not prevent the exception.
The callback is done on another thread (async). And you can/should only update the user interface on/from the main thread. InvokeOnMainThread() makes your code execute on the main thread, which is just what you need.
Ideally the code should run.
Check whether imageviewPhoto1 is nill or not in changeImage method.
If nill then your object is probably released.
If not nill then try writing [self.view setNeedsDisplay]; after setting image , just to see redrawing view can update it or not. Or try whether view is present by setting imageviewPhoto1's background color to something else.
Related
I have a UITableView in my ViewController. To populate it, I have to make an async request that may take up to a second to complete. Where should I put it?
When I tried to make ViewDidLoad async and make a call from there, ViewWillLayoutSubviews throws an error, because it tries to position some elements that weren't assigned yet - due to the fact that not all code from ViewDidLoad was executed yet (I think).
Before awaiting anything in ViewDidLoad you need to setup all your view logic. Otherwise your view initialization will not be finished when ViewDidLoad method returns. That could be a potential cause for ViewWillLayoutSubviews to fail. If it still fails, use a try/catch to make sure your service is working:
public override async void ViewDidLoad()
{
base.ViewDidLoad();
// setup all the view elements here (before the async call)
try
{
var results = await MakeYourAsyncRequest();
InvokeOnMainThread(() =>
{
_tableView.Source = ...; // do something with the results
_tableView.ReloadData();
});
}
catch(Exception ex)
{
// do something with the exception
}
}
Try putting the tableView.ReloadData(); method inside
dispatch_async(dispatch_get_main_queue(), ^{} this might solve your issue.
-:As a general rule, you should try to make sure that all of your UI interaction happens on the main thread. And your data fetching task will work in background. It looked like you were calling reload Data from your
background thread, which seems risky.
Depending on the data I would put the call in the AppDelegate. When the app launches the data should be fetched and saved.
When your UITableview appears it will already have the data ready or maybe an error message since you already know the result of the fetch.
The data may change thats why I would also put the fetch call in viewWillAppear() of your ViewController with the UITableview.
ViewDidLoad() is a method that gets called only once. Also it is called as the first method of the ViewController lifecycle.
It would be good if you read a bit about it VC lifecycle.
You can help yourself by trying it in code with printf("method name").
I am working in Swift. When a user presses a UIButton it calls a function ButtonPressed(). I would like ButtonPressed() to do two things:
Update the UIView by removing the current buttons and texts, then uploading some new text.
Call function TimeConsumingCalculation(). TimeConsumingCalculation is the complicated part of my app and does some calculations which take about 20 seconds or so to complete.
Right now, I have the code in the basic order:
ButtonPressed(){
self.Button.removeFromSuperview()
TimeConsumingCalculation()
}
However, it will not remove the button or do any other UI updates or additions until after the TimeConsumingCalculation is complete. I have read and attempted a few guides on closures and asynchronous functions, but have had no luck. Is there a special property with UIView that is causing it to be updated last?
As a side note - I have already attempted putting all UI actions in a separate function and calling it first. It doesn't work. The time consuming function does not take any variables from the buttons or UI or anything like that.
Thanks!
It seems like timeConsumingCalculation() is blocking the main queue, which is in charge of UI updates. Try calling it like this instead and use the isHidden property to hide the button instead of removing it from the view completely.
ButtonPressed(){
self.Button.isHidden = true
DispatchQueue.global(qos: DispatchQoS.QoSClass.userInitiated).async {
self.timeConsumingCalculation()
}
}
here you call timeConsumingCalculation() asynchronously on a background thread. The quality of service we give it is userInitiated, read more about quality of service classes here
I'm trying to change the title of a button after I call back from a notification but it doesn't respond at all. I checked it's not nil and checked the text Im' assigning and all is good. I made the property type strong instead of weak but no success.
- (void) setButtonTitleFromSelectedSearchResult:(NSNotification *)notif
{
[self popController];
self.sourceMapItem = [[notif userInfo] valueForKey:#"SelectedResult"];
NSLog(#"The Selected Result is: %#", self.sourceMapItem.name);
//Testing
NSLog(#"%#", self.fromButton); // check it's not nil
[self.fromButton setTitle:self.sourceMapItem.name];
}
With WatchKit, if a user interface element isn't currently visible, it cannot be updated. So, if you've presented another interface controller "on top", you can't update any of the presenting controller's interface elements until you've dismissed the presented controller. At that point, you can safely update the presenting controller in its willActivate method.
SushiGrass' method of passing blocks is certainly one valid approach. In my testing, however, I ended up having to manage multiple blocks, and many of the subsequent blocks reversed what earlier queued blocks had accomplished (for example, first changing a label's text to "foo", then "bar", then "foo" again. While this can work, it isn't optimal.
I'd suggest that anyone who is working on a WatchKit app takes a moment to consider how they want to account for off-screen (i.e. not-currently-visible) interface elements. willActivate is your friend, and coming up with a way to manage updates in that method is worthwhile if you're moving from controller to controller.
For what it's worth, I've encapsulated a lot of this logic in a JBInterfaceController subclass that handles a lot of this for you. By using this as a base class for your own interface controller, you can simply update your elements in the added didUpdateInterface method. Unfortunately, I haven't yet had the time to write proper documentation, but the header files and sample project should get you going: https://github.com/mikeswanson/JBInterfaceController
I'm using latest XCode 6.3 and below code working with me.
self.testBtn is bind with Storyboard and its WKInterfaceButton
I also have attached screenshot with affected result.
I'm setting initial text in - (void)willActivate
- (void)willActivate {
[super willActivate];
[self.testBtn setTitle:#"Test"];
[self performSelector:#selector(justDelayed) withObject:nil afterDelay:5.0]
}
-(void)justDelayed
{
[self.testBtn setTitle:#"Testing completed...!!"];
}
If you're using an IBOutlet for the property fromButton be sure that is connected to WKInteface on the storyboard, like below:
I solved this kind of issue by creating a model object that has a property that is a block of type () -> (Void) (in swift). I create the model object, set the action in the block that I'd like the pushing WKInterfaceController to do on completion, and finally pass that model object in the context to the pushed WKInterfaceController. The pushed WKInterfaceController holds a reference to the model object as a property and calls it's completion block when it's done with whatever it needs to do and after func popController().
This worked for me for patterns like what you are describing along with removing rows on detail controller deletion, network calls, location fetches and other tasks.
You can see what I'm talking about here: https://gist.github.com/jacobvanorder/9bf5ada8a7ce93317170
I'm using a UIPageViewController for displaying images, and I download the images asynchronously and throw them into the gallery when they're ready (but you're still able to go to the page in the gallery where the image will be).
My issue is that, when the image does load, I need to supply it for the view controller it corresponds to. Right now, I add it to an NSCache instance, and when UIPageViewController's data source method viewControllerAfterViewController: is called, I check if the image for the view controller being requested has already been downloaded (is in the cache) and if it is, I call initWithImage: on the specific view controller, passing the downloaded image.
My issue is with when viewControllerAfterViewController: is called and the image hasn't been downloaded yet. Right now I just load the view controller without the image, and when the imageDidFinishDownloading: delegate method is called, I try to supply the view controller with the image.
However, it seems that even though UIPageViewController asks for the next view controller the previous gets displayed (i.e.: when I get to the 3rd page, it requests the 4th page's view controller) I'm not able to access this requested view controller that I hand off.
If I access pageViewController.viewControllers, the count of the NSArray is never more than 1. So even though it seems like it should have 2 (the currently showing view controller and the next one that it requested), it only ever has one, the currently visible view controller.
The problem is that I need the other one. The image finishes downloading, and I really only have two options.
Give it to my NSCache, so when the init method is called for the view controller, it is handed off.
In the event that it has already been inited, hand it off to the view controller that's already been inited.
But 1 doesn't always work, as sometimes the init method is called when there's no image yet (it hasn't finished downloading), so we'd sometimes depend on 2, but pageViewController.viewControllers only holds the current visible view controller.
So there's basically this third situation where the view controller has already been inited without an image, and the image finishes downloading and I want to assign it to that view controller, but I'm currently on the one before it, and my only reference to the view controllers in the UIPageViewController is on the current visible one.
How do I handle 3? I need to assign something to a view controller but I can't find any way to access that view controller.
I'd use something like a future from PMConcurrency to do this. A future is just an object that promises to return your image as soon as it's available. So by giving the future to your view controller instead of the image, you don't need to worry about when the image arrives.
Your view controller's initializer would look something like this:
- initWithImageFuture:(PMFuture *)future
{
if (self = [super init]) {
[[future onMainThread] onComplete:^(id result, NSError *error) {
if (!error) {
_image = result;
}
}];
}
return self;
}
By adding an onComplete block to the future, the view controller will receive the image as soon as it's available (immediately if the image is already downloaded). Futures run in the background by default, so you'd use onMainThread to have the result returned on the main thread.
So instead of populating your cache with images, you'd populate it with image futures. There are several ways to create futures, but the block-based one is pretty simple:
PMFuture *imageFuture = [PMFuture futureWithBlock:^id{
UIImage *image = ...; // Your image fetching code here
return image;
}];
[myCache setObject:imageFuture forKey:myImageKey];
I have a similar sort of function but I load the image in the view controller that is displaying the image (the VC handed to the UIPageViewController). In your context I guess I'd had the image ID info and the NSCache object to the VC. That isn't what you want to do of course, but just something to think about.
Originally I tried to handle image loading in the parent like you are. As I recall, I kept a weak pointer to the target view controller. When the image arrived if the VC was still alive I could set the UIImageView property.
I want to show a .gif banner in the top of my app. I'm using this code to show the animated field BB Animated GIF Field. The issue is that I want to refresh this banner every minute. I've tried so many things:
create a new object, but this doesn't change the banner.
create a new animated field and try to replace it....IllegalArgumentException. (I'm trying to change that from inside a Thread, using invokeLater()...I've used invokeAndWait() too)
remove this animated field and put a new one (from the invokeLater() or invokeAndWait() -> IllegalException)
setting a bitmap to this field. The first animate doesn't show and I can see an image from the other banner but it isn't animated.
Any ideas?
If you need to see some code, I will try to post it tomorrow.
If you're using a minimum BlackBerry OS of 6.0 (or above), the BitmapField class directly supports animated gifs.
If you need to support a lower OS version, then you just need to add a method to your AnimatedGIFField class to swap out the old image, and use a new one:
public void setImage(EncodedImage image) {
// Call BitmapField#setImage()
super.setImage(image);
// Store the image and its dimensions.
_image = image;
_width = image.getWidth();
_height = image.getHeight();
_currentFrame = 0;
// Stop the previous thread.
_animatorThread.stop();
// Start a new animation thread.
_animatorThread = new AnimatorThread(this);
_animatorThread.start();
}
Note that this is a UI operation. So, if you want to update the image from a background thread, make sure to wrap it with a call that puts it on the UI thread. For example:
final EncodedImage eImage = EncodedImage.getEncodedImageResource("img/banner.gif");
UiApplication.getUiApplication().invokeLater(new Runnable() {
public void run() {
_animatedBanner.setImage(eImage);
}
});
Update: I also notice that the way the AnimatedGIFField class works is not really good multi-threading practice (for stopping the thread). If you really want to make the code better, you could implement this solution, or
use the technique I show in this answer.
Read more about this here.