Starting with watchOS 2, we have an ExtensionDelegate object, which is analogous to UIApplicationDelegate (reacts to app lifecycle events).
I want to get a reference to the first Interface Controller object, which will be displayed upon launch, to set a property on it (e.g. pass in a data store object).
According to the docs, the rootInterfaceController property on WKExtension hands back the initial controller:
The root interface controller is located in the app’s main storyboard
and has the Main Entry Point object associated with it. WatchKit
displays the root interface controller at launch time, although the
app can present a different interface controller before the launch
sequence finishes.
So I try the following in ExtensionDelegate:
func applicationDidFinishLaunching() {
guard let initialController = WKExtension.sharedExtension().rootInterfaceController else {
return
}
initialController.dataStore = DataStore()
}
Even though the correct Interface Controller is displayed, rootInterfaceController is nil at this point. Interestingly if I query the same property in the willActivate() of my Interface Controller, the property is set correctly.
In an iOS app, you can already get the root view controller in applicationDidFinishLaunching(), and I thought it should work the same for watchOS.
Is there a way to set properties on my Interface Controller before it's displayed (from the outside)? Is this a bug?
Many thanks for the answer!
You might move your code to applicationDidBecomeActive.
This page describes the states of watch apps. When applicationDidFinishLaunching is invoked, the app is in an inactive state.
https://developer.apple.com/library/watchos/documentation/WatchKit/Reference/WKExtensionDelegate_protocol/index.html
If you are calling this from within another interface controller, try move the WKExtension.sharedExtension().rootInterfaceController to the willActivate() function. It seems like if it is in the awake() function it sometimes works but is unreliable.
Related
I'm trying to write a simple to-do list in Swift that will store the list as an array of Strings and then call it back from memory when the app loads.
I've got the following code:
var itemList = [String]()
func loadData() -> [String] {
var arr = [String]()
if NSUserDefaults.standardUserDefaults().objectForKey("storedData") != nil {
arr = NSUserDefaults.standardUserDefaults().objectForKey("storedData")! as! [String]
}
else {
arr = ["Nothing to do..."]
}
return arr
}
func saveData(arr: [String]) {
NSUserDefaults.standardUserDefaults().setObject(arr, forKey: "storedData")
}
Where I'm getting stuck is in where to place the call to loadData(). This is an app that has two view controllers (one for the list, one for an add item setup), so if I place the loadData() call in viewDidLoad() for the main view controller, the array is called back in from memory (and over-written) every time I switch back to the main view controller.
Where is the best place to call this so that it will load once only, upon the app starting up?
the array is called back in from memory (and over-written) every time I switch back to the main view controller.
No. viewDidLoad only loads once, when the app starts. Only viewWillApprear and viewDidAppear get called everytime the viewcontroller changes.
Also you could make your code a bit more compact by using if let:
if let storedData = NSUserDefaults.standardUserDefaults().objectForKey("storedData") as! [String]{
arr = storedData
}
But if you want to make sure to load this only once, you can put it in your AppDelegate file in your applicationDidFinishWithOptions method.
But you'd have to make a variable in your AppDelegate file which you can access from your viewController.
viewDidLoad() only happens when the View Controller is first instantiated. If it is your root view controller you can have it in viewDidLoad().
The other goes, viewDidLoad > viewWillAppear > viewDidAppear. After the view is first loaded only the latter 2 methods are called whenever you navigate.
you can also always register for a NSApplicationDidFinishLaunchingNotification notification at the notification center
check it out here:
https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSApplication_Class/index.html#//apple_ref/c/data/NSApplicationDidFinishLaunchingNotification
Use or overwrite respectively
application(_:didFinishLaunchingWithOptions:)
of your application delegate.
That is called only once upon application launch.
See
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIApplicationDelegate_Protocol/#//apple_ref/occ/intfm/UIApplicationDelegate/application:didFinishLaunchingWithOptions:
viewDidLoad is called after the view for a single controller is first loaded. It shouldn't be called more than once for the life-cycle of a single viewController. Maybe it is possible that, if you are not calling "super.viewDidLoad()" in your own viewDidLoad method, then it may be called again? While you can generally assume that the rootViewController for an application is only created once, I think it's theoretically possible that it might be cleared out of memory by the app if required and then recreated again - so I would never assume it's only called once.
One thing you could do is just set a boolean (default false) to true whenever you load the data and then not call it again if the flag is already set to true.
Alternatively, it's a good idea to separate the data management from your viewControllers. A relatively simple solution would be to have a class called "AppData" say, which might be a singleton (so you can only ever have one instance of it) or a member of your AppDelegate. Then, in your app delegate's "applicationDidFinishLaunchingWithOptions method, you could create the one instance of the AppData class and call the loadData method on it. This class would then live independently of whichever view is currently showing, and the current view could call methods on this object to load/save/update data as required.
I have my WatchKit app (WatchOS1) set up in the following way (names have been changed to be project unspecific):
InitialInterfaceController - The main entry point of the watch app. This controller is only used to load several instances (using the same identifier repeatedly in the NSArray) of the next view using reloadRootControllersWithNames:contexts: (called from awakeWithContext:).
FirstInterfaceController - This Interface controller should be what is first displayed for the pages.
However this does not work - I get left with the blank InitialInterfaceController screen. If however I call [self presentControllerWithNames:contexts:] it works as expected, but includes the cancel button, which is not what I want.
I have seen people suggesting to use this method to dynamically create multiple page navigation scenes, but I cannot see why this doesn't work. The FirstInterfaceController's awakeWithContext: is never called.
Has anybody had this problem or is there a fix available?
Do you have the right value for the Identity in the interface controller's attribute inspector? (I have seen issues when the identity and class name are the same, make sure they are different)
Is that interface controller added to the right module in the identity inspector?
I am basically trying to implement a video conference functionality using opentok.
I have two view controllers.
Class A that has a grey image(to tell user is offline).
It calls setsession from class B to establish the session.
uses ClassADelegate and implements setUserOnlineImage that sets the class A grey image to green.
Class B holds a method useronline.
Has a class method sharedinstance that gives out the singleton instance of the class
viewdidload ->sets a variable type = 2;
setsession ->sets a variable type = 1;
It also has a protocol "ClassADelegate"
Protocol ClassADelegate has method setUserOnlineImage.
Has a callback method session:streamCreated: that is called when a subscriber is created and setupPublisher that publishes the video
The flow is like this.
first Class A calls the setsession from Class B to establish session.
Then when a connect button is clicked the viewdidload is called and then the setupPublisher is called, view is modified loaded and all that.
Now when a subscriber tries to connect session:streamCreated: is called. here when i try to print type value it comes as one, likewise many other variables also become nil which inturn results in just giving the audio and the video isnt seen.
where as if first session:streamCreated: is called (first video is received and then connect is clicked) the flow works fine and the print statement in session:streamCreated: correctly prints type value as 2.
Someone help me figure out whats happening.
I want to know why the type value is getting changed & various other variables become nil. This is preventing the video from showing. Am i missing something? Is any other instance is been taken(but I am using a singleton instance)?
The flow you describe doesn't follow any of the known patterns of how UIViewControllers should behave. Specifically, you shouldn't need to use a singleton instance of a view controller. I think you need to reconsider the architecture, specifically the relationship between these two view controllers.
By the way, the viewDidLoad method is called on the view controller as soon as its view property becomes available, which can be before its on the screen. If the view controller is loading its view from a storyboard or nib, viewDidLoad is called as soon as that view is ready. Otherwise if you are implementing loadView, viewDidLoad is called after that method is finished.
Can you describe what Class A and Class B are trying to accomplish? It sounds like Class A is a view controller for some type of status view that shows a user's online/offline status. Class B sounds like its the OTSessionDelegate as well as the view controller for where the publisher/subscriber views will be placed. Why are these not the same View Controller? (generally view controllers are meant to control a "screenful" of content, unless you are using View Controller Containment). If these two view controllers are not on the screen at the same time, can you use a segue to pass data between them when the transition occurs?
UPDATE:
The additional information is useful for me to give you a recommendation. The thing I'm still uncertain about is if you actually do have these 2 view controllers' views on screen at the same time. This solution should work in both cases.
Outside of a segue, one view controller should not really be calling another view controller's methods directly (so calling setsession as you described is a bad idea). You shouldn't even set one as the delegate of another. At most they should share a Model object to communicate. The OTSession can be seen as a Model object. The challenging limitation is that when using the delegation pattern, only one object (you chose Class B) can be informed of updates. Rather than using the delegation pattern, I think you should use NSNotifications. In order to accomplish this, you should "wrap" the OTSession model in your own model object, setting your own model object as the delegate. Then you can notify both controllers of interesting changes as they happen. I've created a diagram to demonstrate:
In this diagram, all the downward solid arrows are owning references. VideoConference would be your own class and it would implement the OTSessionDelegateProtocol. On initialization, the VideoConference instance would create and own an OTSession instance. When something happens that Class A or Class B need to know about (such as the remote user coming online), VideoConference can send an NSNotification, which both controllers can be observers. Here is a useful article about NSNotifications.
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've found a case where some of my view controllers' initWithCoder methods are invoked before the application didFinishLaunching method in the application delegate. (I've confirmed this by setting breakpoints and looking at the sequence of invocations)
I'm using a storyboard. A UITabBarController is the initial view controller. Part of the problem is that the storyboard creates objects in an unknown order; perhaps it's creating the view controllers before the app is done launching.
In any case, the problem is that I'm registering initial user defaults. This must happen before any piece of the program looks at them. So, I'm trying to find the spot where the registering code will be guaranteed to execute first.
Is there any such place?
Note:
This thread discusses it a little, but there isn't really a conclusion...
ViewDidLoad runs before AppDelegate didFinishLaunchingWithOptions gets executed!
The standard means of initializing user defaults is in a "+(void)initialize" method in your app delegate:
+ (void)initialize
{
if(self == [MyAppDelegate class]) {
...
}
}
This is guaranteed to run before any delegate method gets messaged.
PS: I instantiate a whole bunch of viewControllers in my didLaunch method before returning from that method.
This is natural (and also one of the reasons why using InterfaceBuilder sucks). In application:didFinishLaunchingWithOptions: you generally rely upon the main window and main view controller having already been created from their corresponding NIB/XIB files. Two solutions:
One (preferred): instantiate stuff manually in application:didFinishLaunchingWithOptions:. You can thus control the execution order of any initialization.
Two: use __attribute__((constructor(XXX))) functions - they're guaranteed to be called before main, and the lower the XXX number the earlier the particular constructor function is called. This method is, however, not advisable, because it isn't standard C (only a compiler extension), and it also can easily get very confusing.