I'm using the Mapbox framework on iOS in Swift to create a custom NavigationViewController. I need help getting a reference to the FloatingStackView on the NavigationView.
I've added an additional button (see top left of screenshot) and I'd like to anchor it to the FloatingStackView on the right so that it moves up and down as the top banner changes (similar to the FloatingStackView behavior). The trouble is I can't seem to access the FloatingStackView. From what I've seen in the Mapbox source code the FloatingStackView is a subview of a NavigationView which is the view of a RouteMapViewController which is the mapViewController of a NavigationViewController. Trouble is I can't access the mapViewController as it is inaccessible due to internal protection level.
Here's an example of what I'd like to do:
import UIKit
import MapboxNavigation
class DemoNavigationViewController: NavigationViewController {
override func viewDidLoad() {
super.viewDidLoad()
let mapVC = mapViewController
// Do any additional setup after loading the view.
}
}
Here's the error on the let mapVC line:
'mapViewController' is inaccessible due to 'internal' protection level
My end goal would be to be able to access the FloatingStackView via something like this:
let navigationView = mapViewController.view as? NavigationView
let floatingStackView = navigationView.floatingStackView
If I can get a reference to that floatingStackView I think I can handle the layout.
How can I get around the is inaccessible due to 'internal' protection level error or find another way to access the floating stack view?
From NavigationView class source code
You will not be able to reference it directly if it is marked internal. There is however a hacky way to get to any view and use it for AutoLayout (assuming the view uses autoLayout and has anchors you can use).
As each view has a subViews array you can iterate through the subviews to find the one you want. All subViews are visible, even the ones that come from private instances. The trick is identifying the one you need.
If the Type definition is accessible, you can do something like this
let navView = viewController.subViews.first{$0 is NavigationView}!
let stackView = navView.subViews.first{$0 is UIStackView}!
floatingButton.topAnchor.constraint(equalTo: stackView.topAnchor).isActive true
If you don't have a Type you can match against you'll have to do something even more hacky, such as matching against the description: .first{$0.description conatins "stack"}. Or even by trial and error by indexing the subView array until you get the result you want, and then hoping the order of views is always the same!
Whether you'd want to risk this in production code is only a decision you can make as it subject to breaking if the library changes it's UI design. Depending on the app's user base I might risk matching against type, but probably not the other ways!
Note I've force-unwrapped the views above for brevity - you'd want to be a bit more cautious in production code with that too :-)
Related
In the screen shots below, the only change I have made is to connect an IBOutlet for a constraint. Unconnected, screen lays out correctly. Connected, the screen lays out incorrectly. I have not seen this happen before and don't know what to try to fix it. I need to be able to modify the constant value of the constraint in order to resize a subview depending on the presence or absence of a particular item.
The IBOutlet is declared as:
#IBOutlet weak var tabContentBottomConstraint: NSLayoutConstraint!
So far, I have not implemented any code that reads or modifies this constraint. I added it to my view controller in preparation for using it, but have not gone any further because when I run the code after adding the IBOutlet, the view sizes incorrectly. I have added and removed several times, always with the same result.
Here is the debugger info on the view sizings prior to connecting the outlet:
And here is the debugger info after connecting the IBOutlet with no other code change whatsoever:
The difference in y offset is huge and pushes the view off the screen. As I said above, no code reads or writes to that IBOutlet.
The rolePageDrawerView is item2 in the constraint, and item1 is the view controller's view.safeAreaLayoutGuide. rolePageDrawerView is embedded 3 levels deep into child views of view.safeAreaLayoutGuide.
I found a way to work around this weird behavior by programmatically searching out the constraint with the following code:
guard let tabContentBottomConstraint = view.constraints.filter({ $0.secondItem! as! NSObject == rolePageTabContentView }).first else {
fatalError("unable to find the constraint for drawer sizing")
}
self.tabContentBottomConstraint = tabContentBottomConstraint
I left the constraint as a weak reference even though it is no longer injected since the view itself will have a reference to it to keep it alive. It works.
HOWEVER, I'm leaving the question open for a better solution. The above search is very fragile some the layout change in the future. It would be much better if the framework was injecting the constraint without breaking the layout. Still open for better solutions.
I am using iOS UITest for a Swift application. I use something like,
func testAllScreenNavigation() {
let app = XCUIApplication()
app.tabBars.buttons["Home"].tap()
app.navigationBars["Home"].buttons["More"].tap()
app.sheets.buttons["Cancel"].tap()
}
etc. to navigate some of the specific, tabs, buttons, etc. and switch to respective screens. But i want to navigate each and every screens of my Application (It can be BFS style navigation or DFS style navigation, no matter). Is there any way iOS provides so i can get all navigable elements and then explore deeper and deeper automatically for my App?
I also need to keep trace of which xcuoelement in a screen is already processed and which are not yet processed.
The only way I can think of is using Xcode UI test recorder feature.
While you are recording, navigate through all of your screens via the device/simulator and then the XCUIApplication() variable would be recorded with the appropriate references.
If the button/nav bar/any element has text on it, it will show up in the recorded code or else it will be referenced numerically.
Hope that helps.
Kind regards,
Mukund
I like your idea for getting all views and check whether the layouting and localization for example is fine.
I think you need to specify your criteria for "screens" and how they are accessed.
Basically, one could thing of the following structure
- UITabBarController
-- UISplitViewController
--- UINavigationController
---- UIViewController
----- UIBarButtonItems
----- UIView
----- UIButton
----- UISwitch
----- UITableViewCell
You could now go top down from the UITabBarController to the next controlling instance (might also skip one, e.g. SplitViewControllers on iPhones).
You can use the general property:
XCUIApplication().tabBars
Nevertheless that transition is the problem: How would you get from one ViewController to another and are they all position in the ViewController's View or do you have to loop the subviews of a view.
UIButton -> Touch Up Inside
UISwitch -> Value Changed
UITableViewCell -> DidSelectRowAtIndexPath
UIView -> UILongPressGestureRecognizer
This is how I would basically set it up:
For each UIViewController instance, get the related View (and perform the following call recursively).
Check all the subviews of a view.
For UIViews, go even further and check their subviews
For UIButtons, perform TouchUpInside
and so on.
Make sure to have a condition to stop going deeper, as UITableViews got a lot of subviews or your UIWebViews would of course be set up in a different way.
This way you should be able to navigate through a lot Views in your app hierarchy, but you will need some extensions for UIBarButtonItems, custom Gesture Recognizers and of course also for your "special" controls that might listen to value changes and perform a layout-change.
Accessing specific elements
In addition to the above approach where you simply get an array of elements of a specific type, you can access specific elements (e.g. those where you know they are of a very specific type with certain ValueChangeListeners or something)
To access a specific object in particular, like the TabBar example from above, you can use the accessibilityLabel like so. At first you need to declare the accessibilityLabel in your code or in the .xib-file/.storyboard:
// just to illustrate, so you get an idea:
self.tabBarController.isAccessibilityElement = true
self.tabBarController.accessibilityLabel = "tabBar"
And then do:
let tabBar = XCUIApplication().tabBars["tabBar"]
Here is Apple's documentation for setting these accessibilityLabels:
https://developer.apple.com/library/content/documentation/UserExperience/Conceptual/iPhoneAccessibility/Making_Application_Accessible/Making_Application_Accessible.html
A great way to get the related identifier of an element would be to use the Accessibility Inspector from Apple:
https://developer.apple.com/library/content/technotes/TestingAccessibilityOfiOSApps/TestAccessibilityiniOSSimulatorwithAccessibilityInspector/TestAccessibilityiniOSSimulatorwithAccessibilityInspector.html
Accessing elements in general
To access elements in general, you need to make use of the XCUIElementType of these objects, here you will access the objects based on their classes.
E.g. you could call:
"tabBars", "navBars", "tables", "buttons", and so on from the elements in general.
Still you would be facing the issue with "special controls". As the Apple documentation lacks (imho) some detail about properties and attributes, I do recommend the docs here: https://blog.metova.com/guide-xcode-ui-test/ It provides a great overview of what is accessible and may help you getting some better understanding.
An overview of the available XCUIElementTypes can be found here. Basically, the elementType property is an enumerated value that represents the type of an element. XCUIElementType is a very large enumeration and some of its members do not apply to iOS applications (they apply to MacOS X apps). Some of the more commonly used values are:
Alert
Button
NavigationBar
TabBar
ToolBar
ActivityIndicator
SegmentedControl
Picker
Image
StaticText
TextField
DatePicker
TextView
WebView
https://developer.apple.com/reference/xctest/xcuielementtype?language=objc
Say I have an app that has a fixed layout: one button and a few labels. It makes sense not to use multiple view controllers throughout the app since I have the same button and labels. I don't want to copy and paste them because their look and style will never change. If I decide to change their look later on, I would have to go through every single view controller and this is a bad practice (as it is with copying and pasting code).
Though, I want to be able to let the user go back and forward "layouts" the app, but this code doesn't let me do that:
class MyViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
enterFirstLayout()
}
func enterFirstLayout() {
// do things such as change labels' text property
exitFirstLayout()
}
func exitFirstLayout() {
// do things
enterSecondLayout()
}
func enterSecondLayout() {
// ...
}
}
What can I do to avoid creating multiple view controllers and giving the user the possibility to skip and go back to a specific layout of the app?
I'm assuming that you are trying to achieve something like wizard that you can go back and foreword thru screens.
In this case I would use a UICollectionView with UICollectionViewFlowLayout. Build one custom cell that represents your single 'screen' layout than build a simple datasource as an array of custom objects. Datasource should contains for example all label's content as a String variables.
Nice and elegant code.
You get scrolling animation and swipe gesture detection for free. If you like you can add your custom fancy animation.
This is my solution. Going back to your question, if you want to do this your way, you can for example use subclassing. Build your base view controller class with all layout configured and labels exposed as public, read-only variables, then inherit from this class. Please keep in mind that subclassing is tightest possible coupling.
Other option is to build this view controller once and reuse with different dataset.
So decision is your. I would go in 1, 3, 2 order :)
I'm looking for a way to show a UIView "InventoryView" in 2 view controllers.
I'm working on an inventory system for my game that I trying to make but I need to be able to access it from my main view, where it will go to a InventoryViewController (in this ViewController is my InventoryView) but I also need to be able to access the InventoryView from my BattleViewController where it does not go to my InventoryViewController but where it print the InventoryView on my BattleViewController so I can access everything durning the battle.
Example:
(evrything is dragand drop, the UIView and the UIButtons)
InventoryViewController
class InventoryViewController: UIViewController {
class InventoryView: UIView {
//here are some UIButtons and labels
}
}
BattleViewController
class BattleViewController: UIViewController {
class InventoryView: UIView {
//it should print the Inventory Screen on my BattleViewController
//here are the same properties as it shows in the InventoryViewController
}
}
This is a great example to look at the way OOP programming works best.
Ask yourself the following questions:
What is the purpose of the view?
Are the interactions on the view homogenous across all the instances? (touch events, specific behavior, etc...)
What is the minimum amount of information you need to make the view look the way you want?
Once you have those answers, you can approach the concept of reusability of views safely.
The way to go about it is to subclass UIView, create the necessary elements of your view, setup your constraints (still in the view, either in a nib or programmatically), and implement any behavior that will be consistent across views (For example if the view is a segmented control, every time you click a segment all the others go grey and the one you clicked go blue. Since that's the primary purpose of the segmented control, the code for it should belong to the segmented control).
Chances are you will find the docs very useful: https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIView_Class/
Lastly write a setup method that takes all the information you need and sets up all your graphical elements accordingly. Remember, views should never own their data (they should be templates, the controller will provide the data).
I have no idea what you view looks like but I assume the inventory is represented as an object. Then something like could be a good start:
class InventoryView: UIView {
var inventory: Inventory? {
didSet {
if let newInventory = inventory { //in case we set it to nil
setup(withInventory: newInventory)
}
}
}
private func setup(withInventory inventory: Inventory) {
//do your setup here
}
}
Then in your controller you can call:
let inventoryView = InventoryView()
inventoryView.inventory = myPlayer.inventory
You cannot use a view in two places, at least not within the UI. Every view can be added to only one super view at a time.
If you need the same contents to be displayed twice, create a UIViewController class which's view contains the common UI, create two of those and add them to your UI.
I have a fairly basic MainWindow.xib with a source list-style sidebar. I created it by dragging the Source List template into the window, which already contains two NSTableCellViews: HeaderCell and DataCell.
The latter consists of an icon (using NSImageView) and a label (NSTextField). Instead, I want the label and another, smaller label underneath. In IB, this looks as follows:
If I focus on just DataCell, it highlights accordingly:
Thing is, actually running the program, it looks nothing like the template:
Notice how the two NSTextFields just get smashed together into one. My understanding was that view-based NSOutlineViews (and view-based NSTableViews, for that matter) are supposed to be designed as a template from within IB. Instead, the dimensions from the template seem to get mostly ignored.
Here's the code that sets the view's values from the data source:
public class TourSourceListDelegate : NSOutlineViewDelegate
{
public override bool IsGroupItem(NSOutlineView outlineView, MonoMac.Foundation.NSObject item)
{
return (item as TourSourceListDataSource.Item).IsHeader;
}
public override NSView GetView(NSOutlineView outlineView, NSTableColumn tableColumn, MonoMac.Foundation.NSObject item)
{
if (IsGroupItem(outlineView, item))
{
return outlineView.MakeView("HeaderCell", this);
}
else
{
var data = item as TourSourceListDataSource.Item;
var dataView = outlineView.MakeView("DataCell", this);
(dataView.Subviews[0] as NSTextField).StringValue = data.Name;
(dataView.Subviews[1] as NSTextField).StringValue = data.Date_start.ToShortDateString();
return dataView;
}
}
}
I've tried overriding GetRowHeight, but that doesn't seem to resolve the problem (it makes more room, but still doesn't let the views distribute themselves properly), nor does it seem necessary.
I've also tried playing with the various Autosizing, Autoresizes Subviews, etc. toggles in IB, but that doesn't seem to produce intuitive results, and again, it doesn't seem necessary — the view as presented in IB is exactly what I want, just with slightly longer labels in practice.
I haven't tried converting this to AutoLayout yet.
What obvious step am I missing?
Some more info that probably doesn't make a difference: this is a Xamarin.Mac/MonoMac project with Xcode 5.0, MacOSX10.8.sdk, Xamarin Studio 4.0.12, Xamarin.Mac 4.0.12, and Mono 3.2.3 (targeting Mono / .NET 4.0). I've also enabled App Sandboxing.
What's important in interface builder is the view hierarchy. What kind of view is that cell? Are those labels really subviews of the cellview or not? The hierarchy should look something like:
One thing that's fishy that I see is accessing dataView.Subviews[0] and [1]. If you're adding subviews to your cells then should be creating your own NSTableViewCell subclasses, with each view connecting to the subclass' IBOutlet properties. The subclass doesn't need any code in its implementation, just the declaration of its properties in #interface, such as titleField and descriptionField, and an empty #implementation that auto-synthesizes them.
Then makeViewWithIdentifier (or apprently the glue MakeView in Xamarin) when passed the right identifier should create your NSTableViewCell subclass, and at runtime you can verify that using po dataView in the debugger. Then you access the subviews using the properties of your NSTableViewCell subclass' interface instead of assuming which view is in which position with the subview array, using dataView.titleField and dataView.descriptionField.
If your cell view has one text field then you can use NSTableViewCell without subclassing, but do connect up the textField outlet (its connected by default as long as you don't delete & recreate the cell view's label view) so you can access it through the property, again instead of having to dive into the subviews array.
All that said, it's not really clear why you're seeing what you are. It looks like those aren't the subviews you expect, and might even look like the wrong fonts as well as in the wrong positions. Using a custom subclass of NSTableViewCell and verifying its class at runtime is a good way of making sure it's creating the view you expect, but you can also dump the subview within the debugger using po [dataView _subtreeDescription].