I have issue with implementing data passing between two WatchKit interface controllers. I want to pass data from first interface controller to another one. The first interface controller is working fine without any issue, but the second interface controller not getting the data from the first one.
Here is my struct :
struct gameStruct: Codable {
var id: String
var image: String
var gameName: String
var gameDate: String
var gameVideo: String
var gameSite: String
}
Here is the code in first interface controller:
var gameWatchArray = [gameStruct]()
let getDataURL = "http://ya-techno.com/gameApp/gameData.php"
class InterfaceController: WKInterfaceController {
#IBOutlet var tableView: WKInterfaceTable!
override func awake(withContext context: Any?) {
super.awake(withContext: context)
let URLgame = URL(string: getDataURL)
URLSession.shared.dataTask(with: URLgame!) { (data, response, error) in
do {
guard let data = data else { return }
gameWatchArray = try JSONDecoder().decode([gameStruct].self, from: data)
} catch {
print("error parse")
}
DispatchQueue.main.async {
self.tableView.setNumberOfRows(gameWatchArray.count, withRowType: "gameRow")
for (gameNameIndex, game) in gameWatchArray.enumerated() {
let row = self.tableView.rowController(at: gameNameIndex) as! gameRow
let url = NSURL(string: "http://www.ya-techno.com/gamesImage/\(game.image)")
guard let data = NSData(contentsOf: url! as URL) else { return }
row.gameImage.setImageData(data as Data)
}
for index in gameWatchArray {
index.gameName
index.gameDate
index.image
print("JSON V3 array is :\(index.gameName)")
}
print("JSON V3 array is :\(gameWatchArray.count)")
}
}.resume()
}
override func table(_ table: WKInterfaceTable, didSelectRowAt rowIndex: Int) {
self.pushController(withName: "showDetails", context: gameWatchArray[rowIndex])
}
and here is the Detail interface in my project:
override func awake(withContext context: Any?) {
super.awake(withContext: context)
if let detailData = context as? String {
gameNameLabel.setText(detailData)
}
}
I'm using json to parse data.
The issue is that you are passing a gameStruct instance using self.pushController(withName: "showDetails", context: gameWatchArray[rowIndex]) from your first interface controller to your second one, but then you are trying to cast gameStruct to String, which will obviously fail.
You should modify awake(withContext:) in your second interface controller to conditionally cast context to gameStruct and then access the gameName property of that struct when assigning the String name to the label's text.
In the future you should always handle the cases when a conditional casting fails so that you can find issues more easily by printing a message in case of a failed cast. Or if you are 100% sure that context will always be of a certain type, you can even do force casting, which will enable you to catch programming errors early on in the development stage.
override func awake(withContext context: Any?) {
super.awake(withContext: context)
if let gameDetails = context as? gameStruct {
gameNameLabel.setText(gameDetails.gameName)
} else {
print("Passed context is not a gameStruct: \(context)")
}
}
You should also conform to the Swift naming convention, which is UpperCamelCase for type names, so change gameStruct to GameStruct.
Related
In my swift code below I am trying to save a string into core data using a helper class.
Right now my code is causing a runtime error stating Cannot assign value of type 'Data' to type 'String?' at imageInstance.text = data. I did something similar trying to save a image and it worked. I also added a core data photo
core data pic
class DataBaseHelper {
static let shareInstance = DataBaseHelper()
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
func saveText(data: Data) {
let imageInstance = Info(context: context)
imageInstance.txt = data
do {
try context.save()
print("text is saved")
} catch {
print(error.localizedDescription)
}
}
}
BASE CLASS
class ViewController: UIViewController, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
DataBaseHelper.shareInstance.saveText(data: "Jessica")}
Please be more careful how you name your functions.
You are going to save Text( so the parameter label data is misleading and the type Data is wrong.
Replace
func saveText(data: Data) {
with
func saveText(data: String) {
or – more meaningful because text is already a part of the function name
func saveText(_ string: String) {
let imageInstance = Info(context: context)
imageInstance.txt = string
and call it
DataBaseHelper.shareInstance.saveText("Jessica")
I am receiving three errors in my viewController and I can not figure out how to fix them these errors are 1.) extension Declaration is only valid at file scope, 2.)Value of type 'Unable to infer type of a closure parameter 'result' in the current context ' 3.) URLSession has no member 'request'. None of these errors make any sense to me because I have already defined result as a member of URLSession. Im not sure if Xcode is glitching, or if these errors are present in my code and if so can anyone point them out and how to fix?
import UIKit
import Foundation
enum NetworkError: Error {
case badRequest
case decodingError
}
struct Category: Codable {
let CategoryId: String
let CategoryThumb: String
let CategoryDescription: String
let categories: [Category]
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
struct Constants {
static let categoriesurl = URL(string: "https://www.themealdb.com/api/json/v1/1/categories.php")
static let filterListurl = URL(string: "https://www.themealdb.com/api/json/v1/1/filter.php?c=Beef")
static let mealByIdurl = URL(string: "https://www.themealdb.com/api/json/v1/1/lookup.php?i=52874")
}
//create table view to test results from API endpoint
let table: UITableView = {
let table = UITableView()
table.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
return table
}()
private var categories: [Category] = []
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(table)
table.delegate = self
table.dataSource = self
fetch()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
table.frame = view.bounds
}
func fetch() {
URLSession.shared.request(url: Constants.categoriesurl, returning: [Category].self
) {[weak self] result in
switch result {
case.success(let category):
DispatchQueue.main.async {
self?.category = category
self?.table.reloadData()
}
case.failure(let error):
print(error )
}
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return categories.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = table.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = categories[indexPath.row].CategoryId
return cell
}
}
extension URLSession {
func request<T: Codable>(url: URL?,returning: T.Type,completion: #escaping (Result<T, Error>) -> Void) {
guard let url = url else {
//we create a enum for network error that will return an error if the API end point is a bad request,or if unable decoding the Json data
//Create a urls that fetches the data from each API end points
NetworkError.badRequest
return
}
let task = dataTask(with: url) {data, _, error in
guard let data = data else {
if let error = error {
NetworkError.badRequest
} else {
NetworkError.decodingError
}
return
} //if your data is return sucessfully from the API
do {
let Result = try! JSONDecoder().decode(returning, from: data)
completion(.success(Result))
}
catch {
NetworkError.badRequest
} //if the data does not return an error will be met
}
task.resume()
}
}
}
You're declaring an extension within the scope of a struct declaration. Your extension has to be declared outside of any other scope.
Because you extension was rejected, The system doesn't know what kind of parameter request should take and since you haven't declared the type of result inside the closure, it has no way to infer what what type it should have and the error message tells you just that
URLSession does not have a member named request because your extension was rejected (because of error #1).
The first step to fix this would be to move your extension outside of the scope of struct Category
extension Declaration is only valid at file scope -
Extension go inside a struct or a class, here you are declaring urlSession extension inside Category struct, please declare it outside.
Value of type 'Unable to infer type of a closure parameter 'result' in the current context ' - Please let us know the line of error, mostly this type of error would require to handle do-try-catch.
When the extension is placed outside the struct, this error would go.
As I understand, it is best to only test public methods of a class.
Let's have a look at this example. I have a view model for the view controller.
protocol MyViewModelProtocol {
var items: [SomeItem] { get }
var onInsertItemsAtIndexPaths: (([IndexPath]) -> Void)? { get set }
func viewLoaded()
}
class MyViewModel: MyViewModelProtocol {
func viewLoaded() {
let items = createDetailsCellModels()
updateCellModels(with: items)
requestDetails()
}
}
I want to test class viewLoaded(). This class calls two other methods - updateItems() and requestDetails()
One of the methods sets up the items and the other one call API to retrieve data and update those items. Items array us updated two times and onInsertItemsAtIndexPaths are called two times - when setting up those items and when updating with new data.
I can test whether after calling viewLoaded() expected items are set up and that onInsertItemsAtIndexPaths is called.
However, the test method will become rather complex.
What is your view, should I test those two methods separately or just write this one huge test?
By testing only viewLoaded(), my idea is that the implementation can change and I only care that results are what I expect.
I think the same thing, only public functions should be tested, since public ones use private ones, and your view on MVVM is correct. You can improve it by adding a DataSource and a Mapper that allows you to improve testing.
However, yes, the test seems huge to me, the tests should test simple units and ensure that small parts of the code work well, with the example you show is difficult, you need to divide by layers (clean code).
In the example you load the data into the viewModel and make it difficult to mockup the data. But if you have a Domain layer you can pass the UseCase mock to the viewModel and control the result. If you run a test on your example, the result will also depend on what the endpoint returns. (404, 200, empty array, data with error ...). So it is important, for testing purposes, to have a good separation by layers. (Presentation, Domain and Data) to be able to test each one separately.
I give you an example of how I would test a view mode, sure there are better and cooler examples, but it's an approach.
Here you can see a viewModel
protocol BeersListViewModel: BeersListViewModelInput, BeersListViewModelOutput {}
protocol BeersListViewModelInput {
func viewDidLoad()
func updateView()
func image(url: String?, index: Int) -> Cancellable?
}
protocol BeersListViewModelOutput {
var items: Box<BeersListModel?> { get }
var loadingStatus: Box<LoadingStatus?> { get }
var error: Box<Error?> { get }
}
final class DefaultBeersListViewModel {
private let beersListUseCase: BeersListUseCase
private var beersLoadTask: Cancellable? { willSet { beersLoadTask?.cancel() }}
var items: Box<BeersListModel?> = Box(nil)
var loadingStatus: Box<LoadingStatus?> = Box(.stop)
var error: Box<Error?> = Box(nil)
#discardableResult
init(beersListUseCase: BeersListUseCase) {
self.beersListUseCase = beersListUseCase
}
func viewDidLoad() {
updateView()
}
}
// MARK: Update View
extension DefaultBeersListViewModel: BeersListViewModel {
func updateView() {
self.loadingStatus.value = .start
beersLoadTask = beersListUseCase.execute(completion: { (result) in
switch result {
case .success(let beers):
let beers = beers.map { DefaultBeerModel(beer: $0) }
self.items.value = DefaultBeersListModel(beers: beers)
case .failure(let error):
self.error.value = error
}
self.loadingStatus.value = .stop
})
}
}
// MARK: - Images
extension DefaultBeersListViewModel {
func image(url: String?, index: Int) -> Cancellable? {
guard let url = url else { return nil }
return beersListUseCase.image(with: url, completion: { (result) in
switch result {
case .success(let imageData):
self.items.value?.items?[index].image.value = imageData
case .failure(let error ):
print("image error: \(error)")
}
})
}
}
Here you can see the viewModel test using mocks for the data and view.
class BeerListViewModelTest: XCTestCase {
private enum ErrorMock: Error {
case error
}
class BeersListUseCaseMock: BeersListUseCase {
var error: Error?
var expt: XCTestExpectation?
func execute(completion: #escaping (Result<[BeerEntity], Error>) -> Void) -> Cancellable? {
let beersMock = BeersMock.makeBeerListEntityMock()
if let error = error {
completion(.failure(error))
} else {
completion(.success(beersMock))
}
expt?.fulfill()
return nil
}
func image(with imageUrl: String, completion: #escaping (Result<Data, Error>) -> Void) -> Cancellable? {
return nil
}
}
func testWhenAPIReturnAllData() {
let beersListUseCaseMock = BeersListUseCaseMock()
beersListUseCaseMock.expt = self.expectation(description: "All OK")
beersListUseCaseMock.error = nil
let viewModel = DefaultBeersListViewModel(beersListUseCase: beersListUseCaseMock)
viewModel.items.bind { (_) in}
viewModel.updateView()
waitForExpectations(timeout: 10, handler: nil)
XCTAssertNotNil(viewModel.items.value)
XCTAssertNil(viewModel.error.value)
XCTAssert(viewModel.loadingStatus.value == .stop)
}
func testWhenDataReturnsError() {
let beersListUseCaseMock = BeersListUseCaseMock()
beersListUseCaseMock.expt = self.expectation(description: "Error")
beersListUseCaseMock.error = ErrorMock.error
let viewModel = DefaultBeersListViewModel(beersListUseCase: beersListUseCaseMock)
viewModel.updateView()
waitForExpectations(timeout: 10, handler: nil)
XCTAssertNil(viewModel.items.value)
XCTAssertNotNil(viewModel.error.value)
XCTAssert(viewModel.loadingStatus.value == .stop)
}
}
in this way you can test the view, the business logic and the data separately, in addition to being a code that is very reusable.
Hope this helps you, I have it posted on github in case you need it.
https://github.com/cardona/MVVM
I´ve a webrequest with jsonserialization, after that, a for-in fetch process.
In whole this takes approximately 5-7 seconds.
After that i want to refersh my tableview in Viewcontroller.
The scheme of the function looks like this.
public struct Container {
let name: String
let symbol: String
let rank: String
}
public var dataArray = [Container]()
func fetchNewData() {
var view = ViewController()
// WebbRquest...
// Json serialization...
// the following list is much longer, will take a while...
for items in json {
let name = items["name"] as? AnyObject;
let symbol = items["symbol"] as? AnyObject;
let rank = items["rank"] as? AnyObject;
let result = Container(name: name! as! String, symbol: symbol! as! String,rank: rank! as! String)
dataArray.append(result)
}
// Now, after alle the work is done, i want to reload the tableview in Viewcontrller:
view.reload()
// Here i´m getting error, because nothing will be executed after return.
}
How can I call the reload function, after the webrequest process is finished? Because after the return, the function doesn´t execute anything anymore.
And no other function will "know" when the fetchNewData() function is finished.
Thanks for any help!
#IBAction func updateButton(_ sender: Any) {
fetchNewData()
}
According Phillipps suggestion, I had to modify the #IBAction func a little bit.
But now it´s working. Awesome!
Here the full working version:
public struct Container {
let name: String
let symbol: String
let rank: String
}
public var dataArray = [Container]()
func fetchNewData(completion:#escaping ([Container])->()) {
var view = ViewController()
// WebbRquest...
// Json serialization...
// the following list is much longer, will take a while...
for items in json {
let name = items["name"] as? AnyObject;
let symbol = items["symbol"] as? AnyObject;
let rank = items["rank"] as? AnyObject;
let result = Container(name: name! as! String, symbol: symbol! as! String,rank: rank! as! String)
dataArray.append(result)
}
completion(dataArray)
}
This is the actionFunc:
#IBAction func upDateButton(_ sender: Any) {
let data = dataArray
fetchNewData() {_ in (data)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
Here's a start. It will be vague because I'm making guesses about code I can't see, but you may be able to convert it to your own needs.
Change the fetch function so that it takes a closure as a parameter:
func fetchNewData(completion:([Container])->()) {
...note that the closure will accept the data array when it's called.
After you have your json all parsed, you then invoke the closure:
dataArray.append(result)
}
completion(dataArray)
The "magic" is in the view controller where you tell fetchNewData what to do when it's finished. Something like:
#IBAction func updateButton(_ sender: Any) {
fetchNewData() {(data)
// Save the data where the view controller can use it
self.tableArray = data
// Main queue for UI update
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
Note that the closure is written in the view controller, so self is the view controller. This means no need to create a second (useless) controller inside the fetch.
I'm having trouble with some Swift Optional Binding with a cast into a protocol. I have the following code in a playground that works fine.
protocol CodeCollection {
var name: String { get }
var codes: [String] { get }
}
struct VirtualDoors: CodeCollection {
var name = "Virtual Doors"
var codes: [String] = ["doorNumba1", "doorNumba2"]
}
// Instance of VirtualDoors
let doors = VirtualDoors()
// cast into Any? like what awake(withContext context: Any?) receives
var context = doors as Any?
print(context)
if let newDoors = context as? CodeCollection {
// Works as expected
print(newDoors)
}
I'm using the exact same protocol and struct in watchKit as a piece of info passed in awake(withContext context: Any?) and the optional binding with cast is failing there.
override func awake(withContext context: Any?) {
super.awake(withContext: context)
// Just checking to make sure the expected item is in fact being passed in
print(context)
// "Optional(VirtualDoors(name: "Virtual Doors", codes: ["doorNumba1", "doorNumba2"]))\n"
if let newDoors = context as? CodeCollection {
self.collection = newDoors
print("Context Casting Success")
} else {
// Casting always fails
print("Context Casting Fail")
}
}
I'd be really appreciative if someone could tell me why this is working in the playground but not in the watchKit class method.
I feel like I am missing something really obvious.
I suspect you doors to context as an implicit Any??, which only unwraps to another Optional instead of CodeCollection.
If you use let context = context as AnyObject inside the awake function, then should be able to unwrap it correctly.
Think of it like an force-unwraped optional that you don't get to see.
The last two comments of this playground should give others an example to play with where the optionals are kept, but the optional type is erased and wrapped.
import Foundation
protocol Target {
var name: String { get }
}
func takesAnyOptional(context: Any?) -> String? {
return (context as? Target)?.name
}
struct Source: Target {
let name = "Source"
}
let solid = Source()
print((solid as Target).name)
takesAnyOptional(context: solid)
let solid_any = solid as Any
print((solid_any as? Target)?.name)
takesAnyOptional(context: solid_any)
takesAnyOptional(context: solid_any as Any?)
let solid_anyOpt = solid as Any?
print((solid_anyOpt as? Target)?.name)
takesAnyOptional(context: solid_anyOpt)
takesAnyOptional(context: solid_anyOpt as Any) // -> double optional -> nil
let solid_anyOpt_any = solid_anyOpt as Any
takesAnyOptional(context: solid_anyOpt_any) // -> double optional -> nil