Unit testing a viewController with constructor dependency - swift

I want to write a simple unit test for the viewController described below that has constructor dependency. Unfortunately one of the parameters is of type NSCoder. How do I create the NSCoder argument while testing? As I get this error message.
caught "NSInvalidArgumentException", "*** -decodeObjectForKey: only defined for abstract class. Define -[NSKeyedArchiver decodeObjectForKey:]!"
online research: deprecated solution stackoverflow
ViewModel
class ViewModel {
private let response: DataResponse
private var name = "Character name"
init(reponse: Result) {
self.reponse = response
}
var fullName: String {
return name
}
fund getData() {
if self.result.name.count > 0 {
self.name = self.result.name
}
}
}
ViewController
class ViewController: UIViewController {
#IBOutlet weak var name: UILabel!
private var viewModel: ViewModel
init?(viewModel: ViewModel, coder: NSCoder) {
self.viewModel = viewModel
super.init(coder: coder)
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
viewModel.getData()
characterNameLabel.text = viewModel.fullName
}
}
The method that shows viewController
#IBSegueAction func showViewController(coder: NSCoder) ->
ViewController? {
guard let selectedRow = tableView.indexPathForSelectedRow?.row else {
return nil
}
let singleResult = specialViewModel.results(at: selectedRow)
let viewModel = ViewModel(result: singleResult)
return ViewController(viewModel: viewModel, coder: coder)
}
Testcase snippet
class ViewControllerTest: XCTestCase {
var viewControllerUnderTest: ViewController!
var viewModel: ViewModel!
override func setUp() {
super.setUp()
let data = PagedResponse.stub().data.results[0]
self.viewModel = ViewModel(result: data)
self.viewModel.getData()
let coder = "??" // how do I create coder
self.viewControllerUnderTest = ViewControllerUnderTest(viewModel: viewModel, coder: coder)
self.viewControllerUnderTest.loadView()
self.viewControllerUnderTest.viewDidLoad()
}
override func tearDown() {
super.tearDown()
}
}

Related

required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } [closed]

Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 2 years ago.
Improve this question
I want to create custom tab bar for my open edx ios app but I get this error
can someone please tell me how can I fix this and how can I additional tab bar ]
here is full code
import UIKit
import WebKit
public class CourseHandoutsViewController: OfflineSupportViewController, LoadStateViewReloadSupport, InterfaceOrientationOverriding {
public typealias Environment = DataManagerProvider & NetworkManagerProvider & ReachabilityProvider & OEXAnalyticsProvider
let courseID : String
let environment : Environment
let webView : WKWebView
let loadController : LoadStateViewController
let handouts : BackedStream<String> = BackedStream()
init(environment : Environment, courseID : String) {
self.environment = environment
self.courseID = courseID
self.webView = WKWebView()
self.loadController = LoadStateViewController()
super.init(env: environment)
addListener()
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override public func viewDidLoad() {
super.viewDidLoad()
loadController.setupInController(controller: self, contentView: webView)
addSubviews()
setConstraints()
setStyles()
webView.navigationDelegate = self
}
public override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
environment.analytics.trackScreen(withName: OEXAnalyticsScreenHandouts, courseID: courseID, value: nil)
loadHandouts()
}
override func reloadViewData() {
loadHandouts()
}
override public var shouldAutorotate: Bool {
return true
}
override public var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .allButUpsideDown
}
private func addSubviews() {
view.addSubview(webView)
}
private func setConstraints() {
webView.snp.makeConstraints { make in
make.edges.equalTo(safeEdges)
}
}
private func setStyles() {
self.navigationItem.title = Strings.courseHandouts
}
private func streamForCourse(course : OEXCourse) -> OEXStream<String>? {
if let access = course.courseware_access, !access.has_access {
return OEXStream<String>(error: OEXCoursewareAccessError(coursewareAccess: access, displayInfo: course.start_display_info))
}
else {
let request = CourseInfoAPI.getHandoutsForCourseWithID(courseID: courseID, overrideURL: course.course_handouts)
let loader = self.environment.networkManager.streamForRequest(request, persistResponse: true)
return loader
}
}
private func loadHandouts() {
if !handouts.active {
loadController.state = .Initial
let courseStream = self.environment.dataManager.enrollmentManager.streamForCourseWithID(courseID: courseID)
let handoutStream = courseStream.transform {[weak self] enrollment in
return self?.streamForCourse(course: enrollment.course) ?? OEXStream<String>(error : NSError.oex_courseContentLoadError())
}
self.handouts.backWithStream((courseStream.value != nil) ? handoutStream : OEXStream<String>(error : NSError.oex_courseContentLoadError()))
}
}
private func addListener() {
handouts.listen(self, success: { [weak self] courseHandouts in
if let
displayHTML = OEXStyles.shared().styleHTMLContent(courseHandouts, stylesheet: "handouts-announcements"),
let apiHostUrl = OEXConfig.shared().apiHostURL()
{
self?.webView.loadHTMLString(displayHTML, baseURL: apiHostUrl)
self?.loadController.state = .Loaded
}
else {
self?.loadController.state = LoadState.failed()
}
}, failure: {[weak self] error in
self?.loadController.state = LoadState.failed(error: error)
} )
}
override public func updateViewConstraints() {
loadController.insets = UIEdgeInsets(top: self.topLayoutGuide.length, left: 0, bottom: self.bottomLayoutGuide.length, right: 0)
super.updateViewConstraints()
}
//MARK:- LoadStateViewReloadSupport method
func loadStateViewReload() {
loadHandouts()
}
}
extension CourseHandoutsViewController: WKNavigationDelegate {
public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
switch navigationAction.navigationType {
case .linkActivated, .formSubmitted, .formResubmitted:
if let URL = navigationAction.request.url, UIApplication.shared.canOpenURL(URL){
UIApplication.shared.openURL(URL)
}
decisionHandler(.cancel)
default:
decisionHandler(.allow)
}
}
}
when I change required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
to
super.init(coder: aDecoder)
I get this error
(Property 'self.courseID' not initialized at super.init call)
anyone can help
Thanks
You are not calling the super class' init(coder:) method
Also, if its there, you need to remove the statement fatalError("init(coder:) has not been implemented"
So, replace your init function with following coder method:
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}

Swift MVVM - use protocol to handle viewModel events

I'm trying to use MVVM with delegate protocols. When something changes in the view model I want to trigger it in the view controller.
When I want to use protocols to handle the view model's event on a view controller, I can not set the protocol to the view controller for my view model class.
It gives me the error:
Argument type (SecondViewController) -> () -> SecondViewController does not conform to expected type SecondViewModelEvents
How can I do this the right way?
Here is the code for my view model:
protocol SecondViewModelEvents {
func changeBackground()
}
class SecondViewModel:NSObject {
var events:SecondViewModelEvents?
init(del:SecondViewModelEvents) {
self.events = del
}
func loadDataFromServer() {
self.events?.changeBackground()
}
}
And for my view controller class:
class SecondViewController: UIViewController,SecondViewModelEvents {
let viewModel = SecondViewModel(del: self) //Argument type '(SecondViewController) -> () -> SecondViewController' does not conform to expected type 'SecondViewModelEvents'
#IBAction func buttonPressed(_ sender: Any) {
self.viewModel.loadDataFromServer()
}
func changeBackground() {
self.view.backgroundColor = UIColor.red
}
}
You're trying to initialize the view model variable and pass the view controller as a delegate which at this point is not fully initialized.
Try checking out the very informative and very detailed Initialization page in the official Swift language guide.
Since this is a protocol used for this specific purpose, we can safely constrain it to classes (notice the : class addition to your code.
protocol SecondViewModelEvents: class {
func changeBackground()
}
It's good practice to use more descriptive naming, and also using weak references for delegate objects in order to avoid strong reference cycles.
class SecondViewModel {
weak var delegate: SecondViewModelEvents?
init(delegate: SecondViewModelEvents) {
self.delegate = delegate
}
func loadDataFromServer() {
delegate?.changeBackground()
}
}
You can try to use an optional view model, which will get initialized in an appropriate place, like awakeFromNib():
class SecondViewController: UIViewController, SecondViewModelEvents {
var viewModel: SecondViewModel?
override func awakeFromNib() {
super.awakeFromNib()
viewModel = SecondViewModel(delegate: self)
}
#IBAction func buttonPressed(_ sender: Any) {
viewModel?.loadDataFromServer()
}
func changeBackground() {
view.backgroundColor = UIColor.red
}
}
Or an alternative approach would be to initialize a non-optional view model in the UIViewController required initializer:
// ...
var viewModel: SecondViewModel
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.viewModel = SecondViewModel(delegate: self)
}
// ...
You need to use lazy initialization as,
lazy var viewModel = SecondViewModel(del: self)
OR
lazy var viewModel = { [unowned self] in SecondViewModel(del: self) }()

Generically setting accessibility identifier for automation tools?

We have a requirement in our project. We need to set accessibility identifier for all the components in approximately 40 view controllers. I was thinking how to achieve these basic work by getting each view controller name and iboutlet names in run time and generate ids by combining these values as accessibility id. For these, I need to get IBOutlet's names. How can I do that ? Or do you have any alternative idea for automating this process another way ?
Thanks.
You can try Sourcery
It able to parse all your source files and provide you information about IBOutlets of all controllers:
You interested in classes -> variables -> attributes
You can generate inline for all such variables didSet block in which you will setup proper accessibility identifier
you can check this link and you find a great solution to automate setting accessibilityIdentifier with the same name of the variable
https://medium.com/getpulse/https-medium-com-dinarajas-fixing-developer-inconveniences-ios-automation-e4832108051f
import UIKit
protocol AccessibilityIdentifierInjector {
func injectAccessibilityIdentifiers()
}
extension AccessibilityIdentifierInjector {
func injectAccessibilityIdentifiers() {
var mirror: Mirror? = Mirror(reflecting: self)
repeat {
if let mirror = mirror {
injectOn(mirror: mirror)
}
mirror = mirror?.superclassMirror
} while (mirror != nil)
}
private func injectOn(mirror: Mirror) {
for (name, value) in mirror.children {
if var value = value as? UIView {
UnsafeMutablePointer(&value)
.pointee
.accessibilityIdentifier = name
}
}
}
}
extension UIViewController: AccessibilityIdentifierInjector {}
extension UIView: AccessibilityIdentifierInjector {}
class BaseView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
injectAccessibilityIdentifiers()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class BaseViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
injectAccessibilityIdentifiers()
}
}
How to use it
class ViewController: BaseViewController {
let asd1 = BaseView()
let asd2 = BaseView()
let asd3 = BaseView()
let asd4 = BaseView()
let asd5 = BaseView()
let asd6 = BaseView()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
[asd1, asd2, asd3, asd4, asd5, asd6].forEach {
print($0.accessibilityIdentifier)
}
}
}

Swift: How to add new property to UIImageView class?

I'm afraid I'm relatively new to Swift, but have looked around as best I could and haven't been able to figure out how to do this relatively simple task!
I would like to add a new property called "angle" to the class UIImageView, such that you could use "image.angle". Here's what I've got, having attempted to follow the method of a tutorial I used (note that the required init? part was suggested by Xcode and I am not too sure of what it does):
class selection_image: UIImageView {
var angle = Double()
init(angle: Double) {
self.angle = angle
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Thank you very much for any help!!
class selection_image: UIImageView {
var angle = Double()
// your new Init
required convenience init(angle: Double) {
self.init(frame: CGRect.zero)
self.angle = angle
}
override init(frame: CGRect) {
super.init(frame: CGRect.zero)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Using Swift 4.2
#jasperthedog. You can add a property to a given class using AssociatedObjects of the Runtime using an extension as follows:
In this example, I add an optional viewAlreadyAppeared: Bool? property to UIViewController. And with this, I avoid creating subclasses of UIViewController
extension UIViewController {
private struct CustomProperties {
static var viewAlreadyAppeared: Bool? = nil
}
var viewAlreadyAppeared: Bool? {
get {
return objc_getAssociatedObject(self, &CustomProperties.viewAlreadyAppeared) as? Bool
}
set {
if let unwrappedValue = newValue {
objc_setAssociatedObject(self, &CustomProperties.viewAlreadyAppeared, unwrappedValue as Bool?, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}
}

Encoding/Decoding an array of objects which implements a protocol in Swift 2

I have got a class that inherits from NSObject and I want it to be NSCoding compliant. But I ran into trouble while encoding an array of objects which should implement a protocol.
protocol MyProtocol {
var myDescription: String { get }
}
class DummyClass: NSObject, NSCopying, MyProtocol {
var myDescription: String {
return "Some description"
}
func encodeWithCoder(aCoder: NSCoder) {
// does not need to do anything since myDescription is a computed property
}
override init() { super.init() }
required init?(coder aDecoder: NSCoder) { super.init() }
}
class MyClass: NSObject, NSCoding {
let myCollection: [MyProtocol]
init(myCollection: [MyProtocol]) {
self.myCollection = myCollection
super.init()
}
required convenience init?(coder aDecoder: NSCoder) {
let collection = aDecoder.decodeObjectForKey("collection") as! [MyProtocol]
self.init(myCollection: collection)
}
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(myCollection, forKey: "collection")
}
}
For aCoder.encodeObject(myCollection, forKey: "collection") I get the error:
Cannot convert value of type '[MyProtocol]' to expected argument type 'AnyObject?'
OK, a protocol obviously is not an instance of a class and so it isn't AnyObject? but I've no idea how to fix that. Probably there is a trick that I'm not aware? Or do you do archiving/serialization differently in Swift as in Objective-C?
There's probably a problem with let collection = aDecoder.decodeObjectForKey("collection") as! [MyProtocol], too but the compiler doesn't complain yet…
I've just found the solution myself: The key is to map myCollection into [AnyObject] and vice-versa, like so:
class MyClass: NSObject, NSCoding {
let myCollection: [MyProtocol]
init(myCollection: [MyProtocol]) {
self.myCollection = myCollection
super.init()
}
required convenience init?(coder aDecoder: NSCoder) {
let collection1 = aDecoder.decodeObjectForKey("collection") as! [AnyObject]
let collection2: [MyProtocol] = collection1.map { $0 as! MyProtocol }
self.init(myCollection: collection2)
}
func encodeWithCoder(aCoder: NSCoder) {
let aCollection: [AnyObject] = myCollection.map { $0 as! AnyObject }
aCoder.encodeObject(aCollection, forKey: "collection")
}
}
I know your title specifies Swift 2, but just for reference, for a similar problem I was working on, I found that in Swift 3, you don't need to convert anymore to AnyObject.
The following works for me in Swift 3 (using your example):
class MyClass: NSObject, NSCoding {
let myCollection: [MyProtocol]
init(myCollection: [MyProtocol]) {
self.myCollection = myCollection
super.init()
}
required convenience init?(coder aDecoder: NSCoder) {
let collection = aDecoder.decodeObject(forKey: "collection") as! [MyProtocol]
self.init(myCollection: collection)
}
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encode(aCollection, forKey: "collection")
}
}