My program consists of three relatively-distinct areas: listening on a network for new state, performing network actions, and updating the UI. So respectively I want three classes: StateListener, ActionSender, and ViewController, each chugging along on separate threads.
Would that it were so simple -- the three need to interact. Some states discovered by the StateListener require Actions to be sent by the ActionSender or the UI to be updated by the ViewController. Some responses to Actions require the UI to be updated by the ViewController. Some UI actions require Actions to be performed by the ActionSender.
Currently I do something like this (pseudocode):
/* ViewController.swift */
class ViewController : blah
{
//...
func buttonPressed()
{
// ?! Need to do an action here but I can't
// because actionSender is initialised below...
}
func viewDidLoad()
{
let actionSender = ActionSender(m_view: self)
let actionQueue = OperationQueue()
let stateListener = StateListener(m_view: self,
m_actionSender: actionSender,
m_actionQueue: actionQueue)
let stateQueue = OperationQueue()
stateQueue.addOperation(stateQueue.listen())
}
}
/* StateListener.swift */
class StateListener
{
// ...
func listen()
{
while true
{
var state = waitForNewState()
if shouldActOn(state)
{
m_actionQueue.addOperation(m_actionSender.act())
}
}
}
}
/* ActionSender.swift */
class ActionSender
{
// ...
func act()
{
var reply = sendAction()
OperationQueue.main.addOperation(m_view.m_textBox.append(reply))
}
}
This is fairly hellish and doesn't even do what I want it to do, because I can't have the ViewController perform actions (ActionSender's require a ViewController reference to update the view after the action, but I tried initialising the ActionSender within ViewController.init and I got bizarre errors to do with a Code.init that I hadn't implemented...). I want to get above ViewController and initialise all these OperationQueues and objects wherever ViewController gets initialised, but I can't find where that is...
What I've done above is basically object-reference injection of each object and OperationQueue. I know there are other ways of doing this (a hierarchy of callbacks, NSNotifications) but I'm unsure of which is best.
My question is in two parts:
What is the best (i.e., fastest, easiest to implement and maintain, most idiomatic in Swift) way to get the inter-object and inter-thread communication I desire?
I currently get things going from ViewController's viewDidLoad function, which seems awful (and means I can't get a 'higher-up' perspective of the ViewController. Where should this stuff go? AppDelegate advertises itself as the 'program startup' area, but I can't access the ViewController from there... XCode seems to have hidden the startup of my app from me!
I really appreciate your responses!
This post greatly helped with question 2 and the problem of passing self as a parameter to a data member in an initialiser : http://blog.scottlogic.com/2014/11/20/swift-initialisation.html
Specifically, I can use this pattern:
class Foo : blah
{
var m_bar : Bar!
init() {
// notice I get away with not initialising m_bar
}
func viewDidLoad {
m_bar = Bar(m_foo: self)
m_bar.doYourThing()
}
}
The blog prefers the following, which I feel I should add here out of gratitude to the author, though I prefer the above.
class Foo : blah
{
lazy var m_bar : Bar = {
return Bar(m_foo: self) // notice I can pass in self
}
init() {
// notice I get away with not initialising m_bar
}
func viewDidLoad {
m_bar.doYourThing()
}
}
Related
Heres a caption of my API call:
So, I've got the abilities of the pokemons I needed, but now, idk how to get that data out of my Service class (where I'm doing all the parsing), and send it to my InfoViewController.
My purpose is to fetch that data on some label, and then show the ability names for every poke, according to their ID. Here is a caption of my app:
I wanna add an "Ability" label below Weight, and that's where I wanna assign the data. I have a whole CollectionView with all the pokemons, and the goal is assign the correct ability for each one of them.
I'm kinda struggling for a practical (and less verbose) way to reach this.
I apreciated every comment, any advice and suggestion too. Thanks!
EDIT: Heres my code:
extension InfoController: ServiceDelegate {
func finishedWithPokemonAbilities(abilities: [String], id: Int) {
self.abilities = abilities
self.ids = id
print(abilities)
}
}
You can create a custom Protocol(could call it PokemonServiceDelegate as an example) that your InfoViewController would inherit and implement. On your service object(I'm using PokemonService in the example) create a property with a type of PokemonServiceDelegate and set that property to the view controller that you want to receive the data. After the service finishes fetching the data, update the delegate by passing the data in the function declared in the protocol.
// Protocol your view controller will inherit
protocol PokemonServiceDelegate {
// Function your view controller will implement
func finishedWithPokemonAbilities(abilities: [String])
}
class InfoViewController: UIViewController {
// Reference to the service that makes the request
var service: PokemonService
override func viewDidLoad() {
...
// Set the delegate of the service to self
service.delegate = self
...
}
}
extension InfoViewController: PokemonServiceDelegate {
// Implement the protocol
func finishedWithPokemonAbilities(abilities: [String]) {
// Do something with their abilities here
}
}
struct PokemonService {
var delegate: PokemonServiceDelegate?
// The function that you call to get your abilities
func someUpdateFunc() {
...
let abilities = json[abilities].arrayValue.map {$0["ability"]["name"].stringValue}
delegate?.finishedWithPokemonAbilities(abilities: abilities)
...
}
}
I want to access NSCache from more than one place in my APP, as I'm using it to cache images from an API endpoint.
For example table view 4 and viewcontroller 6 in the diagram below use the same images, so I do not want to download them twice.
Candidate solutions:
Singleton
class Cache {
private static var sharedCache: NSCache<AnyObject, AnyObject>?
static public func getCache () -> NSCache<AnyObject, AnyObject> {
if sharedCache == nil {
self.sharedCache = NSCache()
}
return sharedCache!
}
}
Seems to work fine, but "Singletons are bad" so...
Store the cache in TabViewController
This will tightly couple the views to the view controller so...
Store in the AppDelegate somehow. But isn't this the same as 1? So...
Use dependency injection. But we're in a tab view controller, so isn't this the same as 2?
I'm not sure the right strategy here, so am asking whether there is another method that can be used here.
What I've done Created an App with an example using a NSCache, and explored a singleton solution. Ive tried to use dependency injection but think that it doesn't make sense. I've looked at Stack overflow and documentation, but for this specific circumstance I have found no potential solutoins.
What I've given A minimal example, with a diagram and tested solution that I'm dissatisfied with.
What is not helpful are answers that say NSCache is incorrect, or to use libraries. I'm trying to use NSCache for my own learning, this is not homework and I want to solve this specific instance of this problem in this App structure.
What the question is How to avoid using a singleton in this instance, view controllers in a tab view controller.
First up. Singletons are not inherantly bad. They can make your code hard to test and they do act as dependancy magnets.
Singletons are good for classes that are tools e.g NSFileManager aka FileManger, i.e something that does not carry state or data around.
A good alternative is dependancy injection but with view controllers and storyboards it can be hard and feel very boilerplate. You end up passing everything down the line in prepareForSegue.
One possible method is to declare a protocol that describes a cache like interface.
protocol CacheProtocol: class {
func doCacheThing()
}
class Cache: CacheProtocol {
func doCacheThing() {
//
}
}
Then declare a protocol that all things that wish to use this cache can use.
protocol CacheConsumer: class {
var cache: CacheProtocol? { get set }
func injectCache(to object: AnyObject)
}
extension CacheConsumer {
func injectCache(to object: AnyObject) {
if let consumer = object as? CacheConsumer {
consumer.cache = cache
}
}
}
Finally create a concrete instance of this cache at the top level.
/// Top most controller
class RootLevelViewController: UIViewController, CacheConsumer {
var cache: CacheProtocol? = Cache()
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
injectCache(to: segue.destination)
}
}
You could pass the cache down the line in prepareForSegue.
Or you can use subtle sub-classing to create conformance.
class MyTabBarController: UITabBarController, CacheConsumer {
var cache: CacheProtocol?
}
Or you can use delegate methods to get the cache object broadcast downhill.
extension RootLevelViewController: UITabBarControllerDelegate {
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
injectCache(to: viewController)
}
}
You now have a system where any CacheConsumer can use the cache and pass it downhill to any other object.
If you use the coordinator pattern you can save the cache in the coordinator for your navigation flow and access it from there/init with the cache. It also works nicely since when the navigation flow is removed the cache is also removed.
final class SomeCoordinator: NSObject, Coordinator {
var rootViewController: UINavigationController
var myCache = NSCache<AnyObject, AnyObject>()
override init() {
self.rootViewController = UINavigationController()
super.init()
}
func start() {
let vc = VC1(cache: myCache)
vc.coordinator = self
rootViewController.setViewControllers([vc], animated: false)
parentCoordinator?.rootViewController.present(rootViewController, animated: true)
}
func goToVC2() {
let vc = VC2(cache: myCache)
vc.coordinator = self
rootViewController.pushViewController(vc, animated: true)
}
func goToVC3() {
let vc = VC3(cache: myCache)
vc.coordinator = self
rootViewController.present(vc, animated: true)
}
func goToVC4() {
let vc = VC4(cache: myCache)
vc.coordinator = self
rootViewController.present(vc, animated: true)
}
deinit {
print("✅ Deinit SomeCoordinator")
}
}
I have the following class with a static method that uses MKDirections to calculate custom routes between two coordinates. Once it finishes calculating, the method uses a delegate to pass the route (an MKPolyline object) to the View Controller which adds it to a MapView as an overlay. Every route is assigned a title that determines in which color the route is rendered on the map.
class NavigationInterface {
weak static var routeDelegate: RouteDelegate!
static func addRouteFromTo(sourceCoor: CLLocationCoordinate2D, destinationCoor: CLLocationCoordinate2D, transportTypeString: String)
{
let sourcePlacemark = MKPlacemark(coordinate: sourceCoor)
let destinationPlacemark = MKPlacemark(coordinate: destinationCoor)
//var route = MKRoute()
let request = MKDirectionsRequest()
request.source = MKMapItem(placemark: sourcePlacemark)
request.destination = MKMapItem(placemark: destinationPlacemark)
request.requestsAlternateRoutes = false
//get MKDirectionsTransportType based on String identifier
request.transportType = getTransportType(transportTypeString: transportTypeString)
let directions = MKDirections(request: request)
directions.calculate { (response, error) in
if let directionResponse = response?.routes.first {
let route = directionResponse.polyline
route.title = transportTypeString
print("Got Here")
self.routeDelegate!.didAddRoute(route: route)
}
}
}
The delegate is defined through the following protocol:
protocol RouteDelegate: class {
func didAddRoute(route: MKPolyline)
func didAddBoundary(boundary: MKPolygon)
}
The View Controller implements the delegate as follows:
class MapViewController: UIViewController {
#IBOutlet weak var mapView: MKMapView!
...
override func viewDidLoad() {
super.viewDidLoad()
NavigationInterface.routeDelegate = self
}
extension MapViewController: RouteDelegate {
// delegate Method
// called in Navigation Interface
func didAddRoute(route: MKPolyline) {
mapView.add(route)
}
func didAddBoundary(boundary: MKPolygon) {
mapView.add(boundary)
}
}
Now I have attempted to write a UnitTest which checks whether the delegate method "didAddRoute" returns the correct route
For this purpose I've created a test class "NavigationTests" which implements the RouteDelegate protocol and an test method that calculates a route and then evaluates the route returned from the "NavigationTests" protocol implementation of "didAddRoute":
class NavigationTests: XCTestCase, RouteDelegate {
var routes = [MKPolyline]()
var asyncExpectation: XCTestExpectation?
func didAddRoute(route: MKPolyline) {
routes.append(route)
asyncExpectation?.fulfill()
}
...
func testaddRouteFromTo(){
NavigationInterface.routeDelegate = self
asyncExpectation = expectation(description: "routes returned from delegate method")
NavigationInterface.addRouteFromTo(sourceCoor: CoordinateA, destinationCoor: CoordinateB, transportTypeString: "roadTravel")
let result = XCTWaiter.wait(for: [self.asyncExpectation!], timeout: 2.0)
if result == XCTWaiter.Result.completed {
let route = self.routes.first
XCTAssert(route!.title == "roadTravel", "failed to retrieve correct route")
print(route!.title)
} else {
XCTFail()
}
}
}
Now this test method randomly returns routes from the MapViewController implementation of RouteDelegate instead of the NavigationTests implementation. W
How can I avoid these unwanted references to the MapViewController and why is it created at all since I do not instantiate it in the test?
Ideally I would like to prevent the MapViewController from being instantiated when running this test class since it is not required for the Unit Test.
How can I make sure that only the NavigationTests implementation of the RouteDelegate is used?
Statics vs. tests
Because addRouteFromTo(sourceCoor:destinationCoor:transportTypeString:) is a static method, you have made NavigationInterface.routeDelegate static as well. When your tests run, they are setting a global variable. This means the tests have side effects which last beyond the scope of the tests.
Here are a couple of approaches to prevent this from happening:
a) Create a setUp() and tearDown(). In setUp(), save the old value of NavigationInterface.routeDelegate before overwriting it to self. In tearDown(), restore the old value.
b) Change from statics to an object. As a general rule, statics make things harder to test.
Prefer b). It is safer, and lets the pressure of testability improve your design.
…I don't see any references to MapViewController in your test. Was it created by your application delegate?
How to test an asynchronous call?
Now to your larger question. A test that does actual networking is slow and fragile. It depends on your network conditions. It depends on the back end. It introduces a time lag.
You would be better served by restructuring your code so that you can test the following:
Are you creating the correct MKDirectionsRequest?
Are you handling the response correctly?
This will be expressed in at least 2 tests, but probably more. Once you can independently test response handling, then you can test errors as well as successful responses.
So how do you test "create the response" independently from "handle the response"? By doing this work in separate methods. The tests can then just call these methods.
There is no need to test that Apple makes a network call, or does something on the back end, or sends a response. If you follow this approach, the need for asynchronous tests drops dramatically.
I hope this helps. If you need clarification, please ask. For more thoughts on how "the way Apple shows us to write code isn't good testable design," see https://qualitycoding.org/design-sense/
I have an app which contains several viewControllers. On the viewDidAppear() of the first VC I call a set of functions which populate some arrays with information pulled from a database and then reload table data for a tableView. The functions all work perfectly fine and the desired result is achieved every time. What I am concerned about is how often viewDidAppear() is called. I do not think (unless I am wrong) it is a good idea for the refreshing functions to be automatically called and reload all of the data every time the view appears. I cannot put it into the viewDidLoad() because the tableView is part of a tab bar and if there are some modifications done to the data in any of the other tabs, the viewDidLoad() will not be called when tabbing back over and it would need to reload at this point (as modifications were made). I thought to use a set of variables to check if any modifications were done to the data from any of the other viewControllers to then conditionally tell the VDA to run or not. Generally:
override func viewDidAppear(_ animated: Bool) {
if condition {
//run functions
} else{
//don't run functions
}
}
The issue with this is that the data can be modified from many different viewControllers which may not segue back to the one of interest for the viewDidAppear() (so using a prepareForSegue wouldn't work necessarily). What is the best way to 'check' if the data has been modified. Again, I figured a set of bool variables would work well, but I want to stay away from using too many global variables. Any ideas?
Notification Center
struct NotificationName {
static let MyNotificationName = "kMyNotificationName"
}
class First {
init() {
NotificationCenter.default.addObserver(self, selector: #selector(self.notificationReceived), name: NotificationName.MyNotificationName, object: nil)
}
func notificationReceived() {
// Refresh table view here
}
}
class Second {
func postNotification() {
NotificationCenter.default.post(name: NotificationName.MyNotificationName, object: nil)
}
}
Once postNotification is called, the function notificationReceived in class First will be called.
Create a common global data store and let all the view controllers get their data from there. This is essentially a global singleton with some accompanying functions. I know you wanted to do this without global variables but I think you should consider this.
Create a class to contain the data. Also let it be able to reload the data.
class MyData {
static let shared = MyData()
var data : SomeDataType
func loadData() {
// Load the data
}
}
Register to receive the notification as follows:
static let dataChangedNotification = Notification.Name("DataChanged")
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
}
override func viewDidLoad() {
super.viewDidLoad()
// Establish a way for call activity to notify this class so it can update accordingly
NotificationCenter.default.addObserver(self, selector: #selector(handleDataChangedNotification(notification:)), name: "DataChanged", object: nil)
}
func handleDataChangedNotification(notification: NSNotification) {
// This ViewController was notified that data was changed
// Do something
}
func getDataToDisplay() {
let currentData = MyData.shared.data
// do something
}
// Any view controller would call this function if it changes the data
func sendDataChangeNotification() {
let obj = [String]() // make some obj to send. Pass whatever custom data you need to send
NotificationCenter.default.post(name: type(of: self).dataChangedNotification, object: obj)
}
I can't get the app to execute changeCard() after abc() finishes executing. The two functions are in different classes. Here is the code below. Any help is appreciated!
protocol NetworkControllerDelegate {
func changeCard()
}
class NetworkController {
var networkControllerDelegate: NetworkControllerDelegate?
func abc () {
//do something here
networkControllerDelegate?.changeCard()
}
}
class View: UIViewController, NetworkControllerDelegate {
var networkController = NetworkController()
func changeCard() {
//do something here
networkController.networkControllerDelegate = self
}
}
I've read all the similar questions on stackoverflow for the past few hours but still can't figure it out. Thanks!
Try moving the following line into viewDidLoad of your view controller so that the delegate is set up prior to use.
networkController.networkControllerDelegate = self
Also, you might want to think about making the networkControllerDelegate variable in NetworkController weak so as to avoid any retain cycles.