I want to test a ViewController hierarchy without using XCUITests, because they take a huge ton longer than plain old XCTests.
I've had some success but I can't seem to create a ViewController hierarchy more than one level deep.
Here's a test case demonstrating the issue:
class VCHierarchyTests: XCTestCase {
func test_CanMakeViewControllerHierarchy() {
let baseVC = UIViewController()
let vcToPresent = UIViewController()
let vcOnTopOfThat = UIViewController()
let vcOnTopOfThatOnTopOfThat = UIViewController()
newKeyWindowWith(root: baseVC)
baseVC.present(vcToPresent, animated: false)
vcToPresent.present(vcOnTopOfThat, animated: false)
vcOnTopOfThat.present(vcOnTopOfThatOnTopOfThat, animated: false)
//this passes:
XCTAssertNotNil(baseVC.presentedViewController)
//this fails:
XCTAssertNotNil(vcToPresent.presentedViewController)
//this fails:
XCTAssertNotNil(vcOnTopOfThat.presentedViewController)
}
private func newKeyWindowWith(root viewController: UIViewController){
let window = UIWindow(frame: UIScreen.main.bounds)
window.makeKeyAndVisible()
window.rootViewController = viewController
}
}
Is there a way to construct ViewController hierarchies without XCUITest?
Try PresentationVerifier from ViewControllerPresentationSpy to unit test how view controllers are presented.
It's designed to intercept a single present() call, not several of them. If your use case falls outside what it does, please file an Issue.
Related
I am trying to use that the ARKit imageDetection functionality in a swiftUI project and have troubles to implement the renderer. This is what happened so far:
In Xcode 11.2 one can start a new ARKit project using swiftUI. The UIViewRepresentable protocol is used in the ARViewContainer struct that returns an ARView. An ARView object/var is created inside that struct and this "arView" apparently does have a "session" vobject.
I think I could set up this (AR)-session object like it used to work with SceneKit:
struct ScanARViewContainer: UIViewRepresentable {
makeUIView(context: Context) -> ARView {
//let arView = MyARView(frame: .zero)
// changed this line to the following to have an own renderer
let arView = ARView(frame: .zero, cameraMode: ARView.CameraMode.ar, automaticallyConfigureSession: false)
guard let referenceImages = ARReferenceImage.referenceImages(inGroupNamed: "AR Resources", bundle: nil) else {
fatalError("Missing expected asset catalog resources.")
}
let configuration = ARWorldTrackingConfiguration()
configuration.detectionImages = referenceImages
arView.session.run(configuration, options: [.resetTracking, .removeExistingAnchors])
return arView
}
The code is compiled with no complains and when sending this to the phone the AR session is started and seems to do something.
The next step would be to change the renderer to show detected images. In SceneKit one needed to make use of the ARSCNViewDelegate (Image detection results). This is where I got stuck.
I tried to create an own myARView class first to get access to the ARSessionDelegate hoping for being able to access didadd anchor functions.
class MyARView : ARView, ARSessionDelegate {
required init(frame: CGRect) {
super.init(frame: frame, cameraMode: ARView.CameraMode.ar, automaticallyConfigureSession: false)
self.session.delegate = self
}
}
Then I wanted to uses this "new" class in the ARViewContainer struct:
let arView = MyARView(frame: .zero, cameraMode: ARView.CameraMode.ar, automaticallyConfigureSession: false)
//old : let arView = ARView(frame: .zero, cameraMode: ARView.CameraMode.ar, automaticallyConfigureSession: false)
But the compiler complains about "Type 'ARViewContainer' does not conform to protocol 'UIViewRepresentable'.
Or I get this complain "Type of expression is ambiguous without more context" when declaring the
let arView = MyARView(...'
Does anybody know how to do this correctly?
I think I found good inspiration here about how to "catch" the delegate callbacks.
ARKit & Reality composer - how to Anchor scene using image coordinates
Thanks to Mark D
I would like to perform a test in one of my ViewModels that contains a BehaviorRelay object called "nearByCity" that it is bind to BehaviorRelay called "isNearBy". That's how my view model looks like.
class SearchViewViewModel: NSObject {
//MARK:- Properties
//MARK: Constants
let disposeBag = DisposeBag()
//MARK: Vars
var nearByCity:BehaviorRelay<String?> = BehaviorRelay(value: nil)
var isNearBy = BehaviorRelay(value: true)
//MARK:- Constructor
init() {
super.init()
setupBinders()
}
}
//MARK:- Private methods
private extension SearchViewViewModel{
func setupBinders(){
nearByCity
.asObservable()
.distinctUntilChanged()
.map({$0 ?? ""})
.map({$0 == ""})
.bind(to: isNearBy)
.disposed(by: disposeBag)
}
}
The test that i want to perform is to actually verify that when the string is accepted, the bool value also changes according to the function setupBinders().
Any Idea?
Thank you
Here's one way to test:
class RxSandboxTests: XCTestCase {
func testBinders() {
let scheduler = TestScheduler(initialClock: 0)
let source = scheduler.createColdObservable([.next(5, "hello"), .completed(10)])
let sink = scheduler.createObserver(Bool.self)
let disposeBag = DisposeBag()
let viewModel = SearchViewViewModel(appLocationManager: StubManager())
source.bind(to: viewModel.nearByCity).disposed(by: disposeBag)
viewModel.isNearBy.bind(to: sink).disposed(by: disposeBag)
scheduler.start()
XCTAssertEqual(sink.events, [.next(0, true), .next(5, false)])
}
}
Some other points:
Don't make your subject properties var use let instead because you don't want anybody to be able to replace them with unbound versions.
The fact that you have to use the AppLocationManager in this code that has no need of it implies that the object is doing too much. There is nothing wrong with having multiple view models in a view controller that each handle different parts of the view.
Best would be to avoid using Subjects (Relays) at all in your view model code, if needed, they are better left in the imperative side of the code.
At minimum, break up your setupBinders function so that the parts are independently testable. Your above could have been written as a simple, easily tested, free function:
func isNearBy(city: Observable<String?>) -> Observable<Bool> {
return city
.distinctUntilChanged()
.map {$0 ?? ""}
.map {$0 == ""}
}
I have a storyboard with some text views on it. When I try to localize the storyboard (creating Storyboard.strings file), all of the UITextViews don't localize at all. UILabels are fine.
My config is as follow:
1 Base Storyboard file with 2 Storyboard.strings files
Storyboard Configuration
Storyboard.strings files are ok, its working with UILabels after all:
Storyboard.strings file
Have you found a solution for this problem?
I'm using Xcode 8.3, Swift 3, iOS 10.
You could just make a IBOutlet in Xcode and set the initial UITextView value in the didSet function.
E.g.
#IBOutlet var textView: UITextView {
didSet {
textView.text = NSLocalizedString("CUSTOM_LOCALISED_STRING", comment: "Comment.")
}
}
I ended up with this solution:
In a UIViewController I can access my UITextView-objects and force the localized text from Storyboard.strings to be used.
func fixTextViewStoryboardLocalization() {
guard storyboard != nil else {
return // No need to fix it if storyboard is not presented
}
let storyboardName = storyboard!.value(forKey: "name") as! String
for case let textView as UITextView in view.subviews {
if let ident = textView.restorationIdentifier {
textView.text = NSLocalizedString("\(ident).text", tableName: storyboardName, bundle: Bundle.main, value: "", comment: "")
}
}
}
Create a custom MyViewController (pick whatever name you want), call this function in viewDidLoad:
class MyViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
fixTextViewStoryboardLocalization()
}
}
Make sure the storyboard view controller is from class MyViewController:
MyViewController custom class in Storyboard
In order the code above to work, I need to set the Restoration ID for each UITextView in my Storyboard:
UITextView Restoration ID
Final step - localize the text view in Storyboard.strings:
/* Class = "UITextView"; text = "Base text"; ObjectID = "MyTextView-Restoration-ID"; */
"MyTextView-Restoration-ID.text" = "Localized text comes here";
This works for all my UITextViews in all my storyboards.
I've read the documentation, gone through their wonderful Playground example, searched S.O., and reached the extent of my google-fu, but I cannot for the life of me wrap my head around how to use ReactiveSwift.
Given the following....
class SomeModel {
var mapType: MKMapType = .standard
var selectedAnnotation: MKAnnotation?
var annotations = [MKAnnotation]()
var enableRouteButton = false
// The rest of the implementation...
}
class SomeViewController: UIViewController {
let model: SomeModel
let mapView = MKMapView(frame: .zero) // It's position is set elsewhere
#IBOutlet var routeButton: UIBarButtonItem?
init(model: SomeModel) {
self.model = model
super.init(nibName: nil, bundle: nil)
}
// The rest of the implementation...
}
....how can I use ReactiveSwift to initialize SomeViewController with the values from SomeModel, then update SomeViewController whenever the values in SomeModel change?
I've never used reactive anything before, but everything I read leads me to believe this should be possible. It is making me crazy.
I realize there is much more to ReactiveSwift than what I'm trying to achieve in this example, but if someone could please use it to help me get started, I would greatly appreciate it. I'm hoping once I get this part, the rest will just "click".
First you'll want to use MutableProperty instead of plain types in your Model. This way, you can observe changes to them.
class Model {
let mapType = MutableProperty<MKMapType>(.standard)
let selectedAnnotation = MutableProperty<MKAnnotation?>(nil)
let annotations = MutableProperty<[MKAnnotation]>([])
let enableRouteButton = MutableProperty<Bool>(false)
}
In your ViewController, you can then bind those and observe those however necessary:
class SomeViewController: UIViewController {
let viewModel: Model
let mapView = MKMapView(frame: .zero) // It's position is set elsewhere
#IBOutlet var routeButton: UIBarButtonItem!
init(viewModel: Model) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
routeButton.reactive.isEnabled <~ viewModel.enableRouteButton
viewModel.mapType.producer.startWithValues { [weak self] mapType in
// Process new map type
}
// Rest of bindings
}
// The rest of the implementation...
}
Note that MutableProperty has both, a .signal as well as a .signalProducer.
If you immediately need the current value of a MutableProperty (e.g. for initial setup), use .signalProducer which immediately sends an event with the current value as well as any changes.
If you only need to react to future changes, use .signal which will only send events for future changes.
Reactive Cocoa 5.0 will add UIKit bindings which you can use to directly bind UI elements to your reactive layer like done with routeButton in the example.
as we know, to implement PageTabBarController, we need to insert these code in AppDelegate.swift:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions:
let viewControllers = [MatchDetailViewController(),ListPlayersViewController(),ChatViewController()]
window = UIWindow(frame: Device.bounds)
window!.rootViewController = MatchViewController(viewControllers: viewControllers, selectedIndex: 0)
window!.makeKeyAndVisible()
}
Now, i need to use PageTabBarController when i want to open detail for my match data. My question is, how to implement it without insert those code in AppDelegate.swift because it will open my MatchViewController (extend from PageTabBarController) for the first app launch.
I have tried this code, but it will cause Crash, and it pointed to my AppDelegate.swift
class MatchViewController: PageTabBarController {
var window: UIWindow?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
open override func prepare() {
super.prepare()
let viewControllers = [MatchDetailViewController(),ListPlayersViewController(),ChatViewController()]
//1st try: Crash
window = UIWindow(frame: Device.bounds)
window!.rootViewController = MatchViewController(viewControllers: viewControllers, selectedIndex: 0)
window!.makeKeyAndVisible()
//2nd try: error
self.rootViewController = MatchViewController(viewControllers: viewControllers, selectedIndex: 0)
//3rd try: crash
self.viewControllers = viewControllers
delegate = self
preparePageTabBar()
}
fileprivate func preparePageTabBar() {
pageTabBar.lineColor = Color.blue.base
pageTabBar.dividerColor = Color.blueGrey.lighten5
pageTabBarAlignment = PageTabBarAlignment.top
pageTabBar.lineAlignment = TabBarLineAlignment.bottom
}
}
extension MatchViewController: PageTabBarControllerDelegate {
func pageTabBarController(_ pageTabBarController: PageTabBarController, didTransitionTo viewController: UIViewController) {
}
}
Linked GitHub Question
Hi, yes there is a way. The PageTabBarController is inherited from aUIViewController`, which allows you to add it as a child of any other UIViewController. That said, you just gave me a great idea. I am going to make a new UIViewController that allows you to add as many child UIViewControllers, which will make this super easy to do. I will make this as a Feature Request.
Until the update, please use the suggested method of adding it as a child UIViewController. Are you familiar with how to do that?
First create AppToolbarController (subclass of ToolbarController) or you can use the one in the Material library demo.
And then from your view controller, you can use:
DispatchQueue.main.async {
let tabbarViewController = AppPageTabbarController(viewControllers: [vc1,vc2,vc3], selectedIndex: 0)
self.present(AppToolbarController(rootViewontroller: tabbarViewController))
}