We just added Three20 to our existing project and are having some trouble to get along with its routing system.
What we have right now is a transition from a view controller A to a view controller B, having A as the delegate of view controller B, to allow me to trigger an action on A when a specific action occurs on B.
Let's say I have a button on A that calls B like the following:
- (IBAction)buttonAction:(id)sender {
id bvc = [[[BViewController alloc] initWithDelegate:self] autorelease];
[self.navigationController pushViewController:bvc animated:YES];
}
Now, instead of the button, we want to use a link in a TTStyledText for the same purpose. The problem is we don't know how to pass the A view controller instance to B's initWithDelegate: method. We discovered how to pass strings as parameters for the TTURLMap, but that won't work for us.
Maybe we need a more thorough design change here. Any thoughts?
Here are 2 options:
you set all your object in a kind an object container and put it in a global cache where you can get an id for that container which you encode into a string in the url. This would add a bit of overhead in managing the lifecycle of these object :(
There is another useful way to invoke three 20 controller using urls which involves a bit different way of coupling the paramters:
Your target controller would have another init method like this:
-(id)initWithNavigatorURL:(NSURL *)URL query:(NSDictionary *)query {
self = [super initWithNavigatorURL:URL query:query];
if (self) {
self.parameter = [query objectForKey:#"[YOUR PARAMETER NAME]"];
}
return self;
}
you invoke it by calling the controller like this:
TTURLAction *action = [[[TTURLAction actionWithURLPath:#"yourapp://yourViewController"]
applyAnimated:YES]
applyQuery:[NSDictionary dictionaryWithObject:[YOUR OBJECT] forKey:#"[YOUR PARAMETER NAME]"]];
[[TTNavigator navigator] openURLAction:action];
Now all you are left with is mapping the links inside the label to somehow invoke that fancy TTURLAction.
You do this by changing the TTURLMap in your controller that hosts the styled label.
three20 has the following way to add a mapping to a specific class and selector:
- (void)from:(NSString*)URL toObject:(id)object selector:(SEL)selector;
so in that view controller that hosts the label add this:
TTNavigator* navigator = [TTNavigator navigator];
TTURLMap* map = navigator.URLMap;
[map from:#"yourapp://someaction" toObject:self selector:#selector(userpressedlink)];
inside that userpressedlink method call the fancy TTURLAction
A few thing to remember:
you need to remove that mapping ( I suggest viewWillAppear to add the mapping and viewWillDisappear to remove them)
each link should have it's own mapping so you can distinguish the links and map them to differen selectors.
Related
I have a View-Hierarchy like this:
UIViewController (SingleEventViewController)
UIScrollView (EventScrollView)
UIView (contentView)
3xUITableView (SurePeopleTV, MaybePeopleTV, NopePeopleTV (all inherited from the same UITableView)), & all other UI-Elements
The SingleEventViewController stores one Event (passed within the initializer). (All Events are stored in Core-Data).
The three UITableViews are there for displaying the users which are participating (or not or maybe) at the Event. My question is, what are the possibilities to fill the tableViews with the data and what would you recommend in which situation.
Currently I have a property parentVC: SingleEventViewController in all Subviews and get the data like this:
override func loadUsers() {
//class SurePeopleTV
guard let parentController = parentVC else { return }
users = (parentController.thisEvent.eventSureParticipants?.allObjects as! [User])
finishedLoading = true
super.loadUsers()
}
.
func applyDefaultValues() {
//class EventScrollView
guard let parent = parentVC else { return }
titleLabel.text = parent.eventName
}
I'm new to programming but I got a feeling that I should not create a parentVC reference in all of my classes.
An object should not (ideally) know about its parent - if it does they are "tightly coupled". If you change the object's parent, your code may break. In your case, your parent object must have a thisEvent property.
You want your objects to be "loosely coupled", so the object doesn't know about a specific parent object.
In Swift, the usual ways to pass information "back up the chain" is to use the delegate design pattern… https://developer.apple.com/documentation/swift/cocoa_design_patterns or to use closures.
See also https://www.andrewcbancroft.com/2015/04/08/how-delegation-works-a-swift-developer-guide/ for info on delegation
First of all, if you create a reference to the parent ViewController make sure it is weak, otherwise you can run into memory management issues.
Edit: As Ashley Mills said, delegates the way to handle this
The recommended way to pass data between ViewControllers is using something like this
Every time a segue is performed from the view controller this function is in this function is called. This code first checks what identifier the segue has, and if it is the one that you want, you can access a reference to the next view controller and pass data to it.
A common situation is to have a View Controller A, and it has some information which will be sent to View Controller B; and B will edit the information, when B finishes editing the information, B will call the delegate method to update A, and pop itself from the navigation controller.
How to handle this problem with MVVM and ReactiveCocoa?
Heavy use of ReactiveCocoa, in general, will start pushing you away from the delegate pattern. However, since much of the code you've already written and all of the code you'll encounter in the iOS standard libraries use it, being able to interact with it is still important.
You'll want to use the -[NSObject rac_signalForSelector:] category, that will return a signal that receives a RACTuple value of the arguments to a method each time it is invoked, and completes when the object sending the signal is deallocated.
Let's say you have a UIViewController to display that contains a list of checkboxes a user can select, with a continue button at the bottom. Since the selections change over time, you could represent it as an RACSignal of NSIndexSet values. For the purposes of this example, let's say you must use this class as is, and it currently declares a delegate pattern that contains the following:
#class BSSelectionListViewController;
#protocol BSSelectionListViewControllerDelegate <NSObject>
- (void)listChangedSelections:(BSSelectionListViewController*)list;
- (void)listContinueTouched:(BSSelectionListViewController*)list;
#end
When you present the view controller from elsewhere (like a UIViewController at the top of the navigation stack), you'll create the view controller and assign self as the delegate. It might look something like
BSSelectionListViewController* listVC = [[BSSelectionListViewController alloc] initWithQuestion:question listChoices:choices selections:idxSet];
listVC.delegate = self;
[self.navigationController pushViewController:listVC];
Before pushing this UIViewController on the stack, you'll want to create signals for the delegate methods that it could call:
RACSignal* continueTouched = [[[self rac_signalForSelector:#selector(listContinueTouched:)]
takeUntil:list.rac_willDeallocSignal]
filter:^BOOL(RACTuple* vcTuple)
{
return vcTuple.first == listVC;
}];
RACSignal* selections = [[[[self rac_signalForSelector:#selector(listChangedSelections:)]
takeUntil:list.rac_willDeallocSignal]
filter:^BOOL(RACTuple* vcTuple)
{
return vcTuple.first == listVC;
}]
map:^id(RACTuple* vcTuple)
{
return [vcTuple.first selections];
}];
You can then subscribe to these signals to do whatever side effects you need. Maybe something like:
RAC(self, firstChoiceSelected) = [selections map:^id(NSIndexSet* selections)
{
return #([selections containsIndex:0]);
}];
and
#weakify(self)
[continueTouched subscribeNext:^(id x)
{
#strongify(self)
[self.navigationController popToViewController:self];
}];
Because it's possible that you might have several of these screens that you're the delegate of, you want to make sure that you are filtering down to just this one in your RACSignals.
ReactiveCocoa will actually implement these methods (the ones in the delegate protocol) for you. However, to keep the compiler happy, you should add stubs.
- (void)listChangedSelections:(BSSelectionListViewController *)list {}
- (void)listContinueTouched:(BSSelectionListViewController*)list {}
This is, IMO, an improvement over the standard delegate pattern, where you would need to declare an instance variable to hold the selection view controller, and check in the delegate methods which controller is calling you. ReactiveCocoa's rac_signalForSelector method can reduce the scope of that state (this view controller comes and goes over time) in to a local variable instead of an instance variable. It also allows you to be explicit about dealing with the changes to the selections.
In the settings section of my app the user has the option to change the language of the app. So when the user chooses spanish as his primary language the app show the content in spanish after he did an app restart but I want to change the language on the fly. This works for the main content like a TableView because I simply can reload the data but the language in my TabBarController does not change because I don't know how.
So I want to update (or better call it a reset) the TabBarController. After the reset it should display all navigation points in the new language.
My idea was to remove the current TabBarController and initialize a new one. Is this possible? Or is there a better way?
I am not an native english speaker so if my explanations aren't clear enough, just tell me and I'll try to rephrase them.
It might look scary and complicated because of my long post, But it really isn't, it is just long because I thought it would be better to also explain how to do it, instead of just giving a few lines of code.
You can achieve what you want using UITabBarController properties.
UITabBarController have a property called tabBar, which is the actual UITabBar.
One might think that in order to achieve what you want, you should edit this property,
HOWEVER, editing this property would cause an exception.
From apple's UITabBarController documentations, regarding the tabBar property:
You should never attempt to manipulate the UITabBar object itself stored in
this property. If you attempt to do so, the tab bar view throws an exception.
So you should never attempt to edit this property at runtime.
After that word of warning, here is what you should do-
UITabBarController also have a property called viewControllers, which is an NSArray who holds reference to the view controllers that being displayed by the tab bar.
This property CAN be modified at runtime, and changes applied to it are updated instantly in the tab bar.
However, for your case, you don't need to modify this property,
But I thought you should know that so if in some situation you will need to add or remove some items from your tab bar, you'll know that can do it.
What you do want to do, is iterate through the objects of that array to access the view controllers themselves.
UIViewController have a property called tabBarItem which represents the UITabBarItem of the view controller.
So what we are basically doing, is getting the tab bar item of the view controller, but instead of getting it from the UITabBarController itself, we are getting it directly from each view controller.
Each UITabBarItem has a title property, and this is what you want to change.
So now, after that long introduction, let's get to the actual code.
I think a pretty easy way to achieve what you want is to iterate thru the viewControllers array, and have some switch statement in there that would change the title.
As in any programming situations, this can be done in countless other ways, so you might have a better way to implement it than my example below, but this should do the trick.
Each view controller that being displayed in a tab bar controller, have a reference to that tab bar using the property tabBarController
So you can run this code in any of the view controllers that being displayed in the tab bar, and simply use self.reference to get a reference to it.
Add this somewhere after the language have changed-
for (int i = 0; i < [self.tabBarController.viewControllers count]; i++) {
if([self.tabBarController.viewControllers[i] isKindOfClass: [UIViewController class]]) {
UIViewController *vc = self.tabBarController.viewControllers[i];
switch(i) {
case 0:
vc.tabBarItem.title = #"primero";
break;
case 1:
vc.tabBarItem.title = #"secondo";
break;
}
}
}
What we are basically doing, is running a for loop that iterating thru all of the items in the array,
The items in the array are in the same order that they appear on the tab bar,
then we use a switch statement to change the title for the view controller in the corresponding position,
Since array have index 0, the first view controller is at position i=0 and the last one is at one less than the count of items in the array.
Some might argue that my if is unnecessary,
Since we already know that this array holds only view controllers, there is no need to check if the item at that position is of UIViewController class, or a subclass of it.
They might be right, but I always say it's better to be safe than sorry.
Of Curse I would also include in your code something to actually check to what language the user have chosen.
The example above changes the titles to spanish, regardless of the user's choice.
Hope it helps mate,
Good luck
#AMI289 gave a good idea.
Make an extension for UITabBarController and do a loop there. Can call anywhere from the tabBarControllers stack.
In my case after the tabBarController goes navigationController.
// MARK: - UITabBarController
extension UITabBarController {
func changeTitleLocale() {
guard let viewContollers = self.viewControllers else { return }
for (index, navVC) in viewContollers.enumerated() {
if let view = navVC as? UINavigationController {
if let topView = view.topViewController {
if topView.isKind(of: ProfileVC.self) {
self.tabBar.items?[index].title = "tab_profile"
} else if topView.isKind(of: ChatVC.self) {
self.tabBar.items?[index].title = "tab_chat"
} else if topView.isKind(of: PicturesVC.self) {
self.tabBar.items?[index].title = "tab_pictures"
} else if topView.isKind(of: VideosVC.self) {
self.tabBar.items?[index].title = "tab_videos"
}
}
}
}
}
}
Then, when we change a language just run it:
self.tabBarController?.changeTitleLocale()
I have a UIViewController (let's call it "EditViewController") which has a Container View on it (call it "ContainerView") where I switch in and out various subviews (call the one I'm most concerned with "EditDetailsView").
From the EditDetailsView I need to change the title in the navigation bar of the EditViewController. I can't seem to be able to figure out how to reference it.
From inside EditViewController I can simply make a statement like:
self.title = #"Some new title";
and it changes just fine.
But from the EditDetailsView view that is currently the subview of ContainerView nothing seems to work:
self.title = ... is obviously wrong.
super.title = ... doesn't work and seems wrong anyway.
super.super.title = ... errors out as super is not a property found on UIViewController.
Can someone please tell me how to reference the title? I'm kinda lost.
Thanks!
While digging through the parentViewController chain is possible, it is error prone and unrecommended. It is considered a bad design. Imagine you set up your view controller hierarchy in some manner, but after a few months change it a bit and now there is one level deeper. Or, you would like to use the same view controller in several different scenarios. A much better design would be to pass the new title to the container view controller using delegation. Create a delegate protocol, with a method for setting the title.
- (void)childViewController:(ChildViewController*)cvc didChangeToTitle:(NSString*)title;
I know this is an old thread, but in case someone else needs it: to avoid boilerplate code with delegation, and avoid digging into the parentViewController, I did it the other way around.
I've referenced the child view controller from the parent and got their title. So no matter which child you show, you will always get the right title.
This is in Swift 3.
So, basically, this is your parent:
class EditViewController: UIViewController {
override func viewDidLoad() {
if let child = self.childViewControllers.first {
self.title = child.title
}
}
}
And this is your child:
class ContainerView: UIViewController {
override func viewDidLoad() {
self.title = "Sbrubbles"
}
}
Another good way to avoid excess code with delegation is to use RxSwift, if you are familiar to Reactive programming.
I am a new Obj C programmer so please bear with me. My data structure is as follows:
A bill contains a receipt and an array. A receipt contains an array of
receiptItems. ReceiptItems contain a string called itemName.
I am passing a custom object of type Receipt created in one view controller to another view controller. I am doing so through a custom init method. The code is below:
- (id)initWithReceipt:(Receipt *)currentReceipt
{
self = [super initWithNibName:#"SplitCheckViewController" bundle:nil];
if (self) {
ReceiptItem *temp1 = (ReceiptItem *)[currentReceipt.receiptItemsArray objectAtIndex:0];
NSLog(temp1.itemName);
currentBill.originalReceipt = currentReceipt;
ReceiptItem *temp2 = (ReceiptItem *)[self.currentBill.originalReceipt.receiptItemsArray objectAtIndex:0];
NSLog(temp2.itemName);
}
return self;
}
currentReceipt is being passed through and contains the appropriate data per the first NSLog output. However, once I try to assign this object to a property in this view controller (currentBill.originalReceipt) I get a blank output for the second NSLog. I'm sure it is something basic but I am perplexed.
Did you forget to allocate currentBill. Try to print currentBill after the first NSLog.
From your title, You want to pass object from first view controller to second view controller. There are so many ways to do so.
Use singleton class set value in the first view controller and retrieve the value in second view controller.
Store values using keychain, sqllite, core data, plist or any ways you can store.
Most commonly using is when you initialise second view controller, give value to second view controller's property, and then show second view controller
If want to get value from second view controller in the first view controller, then commonly using is delegate.
I believe there are other ways to do it.