Swift: array is set but is then empty when accessed - swift

I have an array which is set once a specific API is called. However when I then try to access this array at a later point it is empty again.
The class in question looks like this:
class SearchableLoginFormField: LoginFormField {
weak var delegate: PopoverPresentableDelegate?
var selectedObject: Selectable?
let popoverType: PopoverType
var sourceList = [Selectable]() {
didSet {
// Field set
}
}
private lazy var selectionPopover: ContainerPopover = {
let popover = LoginFormPopover(objectSelected: { object in
self.selectedObject = object
self.text = object.selectionName
self.selectionPopover.dismiss(animated: true)
}, popoverType: self.popoverType)
return popover
}()
init(popoverType: PopoverType, fieldTitle: String, fieldIcon: UIImage?,
colorScheme: UIColor?, returnAction: (() -> Void)?) {
self.popoverType = popoverType
super.init(fieldTitle: fieldTitle, fieldIcon: fieldIcon, colorScheme: colorScheme, returnAction: returnAction)
configureFormField()
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configureFormField() {
textDidChangeAction = { [weak self] in
/// We clear the selectedAirport each time user searches in field so that we know whether we need to trigger shouldEndEditingAction or not
/// (which we only trigger if user ends action and selectedAirport is nil)
self?.selectedObject = nil
self?.configureField(with: I6TextInputKeyboardSettings(
capitalization: .allCharacters,
spellCheck: .no
))
guard let self = self else { return }
self.searchObjects(
field: self,
popover: self.selectionPopover,
sourceList: self.sourceList,
dataPopover: self.selectionPopover)
}
self.shouldEndEditingAction = { [weak self] in
if self?.selectedObject == nil {
let filteredText = self?.text.replacingOccurrences(of: "-", with: "") // We remove the "-" if user has entered it so that reg can still be validated
self?.text = filteredText ?? ""
self?.verifyFieldInput()
}
}
}
private func searchObjects<T>(field: I6TextField, popover: ContainerPopover, sourceList: [T], dataPopover: ContainerPopover) {
if field.text.isEmpty {
dataPopover.dismiss(animated: true, completion: nil)
} else {
let filterCompletion: (Int) -> () = { count in
dataPopover.sourceView = field
// Present if needed
let isPopoverPresented = dataPopover.isVisiblyPresented
if (!dataPopover.isBeingPresented && !isPopoverPresented && count > 0) {
self.delegate?.presentPopover(popover: dataPopover)
}
if (isPopoverPresented || dataPopover.isBeingPresented) && count == 0 {
dataPopover.dismiss(animated: false, completion: nil)
}
}
dataPopover.filterToSearchTerm(field.text.replacingOccurrences(of: "-", with: ""), objects: sourceList, completion:filterCompletion)
}
}
private func verifyFieldInput() {
let matchingList = sourceList.filter {
$0.selectionName.lowercased() == self.text.lowercased()
}
if matchingList.count == 1 {
self.text = matchingList[0].selectionName
self.selectedObject = matchingList[0]
self.hasError = false
} else if matchingList.count > 1 {
self.errorAlert(errorText: Strings.EHandshake.FlightLookupCustomisable.mulitpleMatches.localizedFormat(""))
} else if matchingList.isEmpty {
self.errorAlert(errorText: Strings.EHandshake.FlightLookupCustomisable.noSelectionMatches.localizedFormat(""))
}
delegate?.textInputValidated(popover: selectionPopover)
}
}
The variable I am focussed on is the sourceList var.
Now, in the class where this object is created we declare the SearchableLoginFormField as follows:
lazy var iata: SearchableLoginFormField = {
let field = SearchableLoginFormField(
popoverType: .airport,
fieldTitle: FlightLookupStrings.originAiportCode.localized,
fieldIcon: UIImage.Login.origin,
colorScheme: fieldColor,
returnAction: nil)
field.delegate = self
validator.registerField(field, rules: [RequiredRule(message: ValidationStrings.aircraftRegistrationRequired.localized)])
return field
}()
And then we set the sourceList with the following delegate method:
func airportsSet() {
iata.sourceList = CoreDataObjectsManager.shared.airportsList
}
This is called when the airports list is retrieved from an API call.
Break points prove that the sourceList is being set correctly:
airportsSet() delegate method is hit and contains the correct airport list
didSet method on the SearchableLoginFormField is fired and the sourceList is successfully set to the Airports list
when we hit the textField delegate method upon editing the field, the sourceList is empty
I tried moving the configureFormField() method into the didSet on the sourceList but I have the same result.
I'm really confused how this seems to be set but then empty again. I'm keeping the breakpoint on the didSet and at no point is it being set to empty by anything else.
The superclass contains the following var:
public var textDidChangeAction: (() -> Void)?
and the following textFieldDelegate method:
open func textFieldDidChangeSelection(_ textField: UITextField) {
didChangeSelectionAction?()
}
So in the configureFormField method we are setting the action of this delegate method accordingly which gets triggered. It is at this point that the sourceList is empty.
The field itself is added in viewDidLoad of the main display viewController as follows:
stackView.add(arrangedSubviews: [number, reg, iata, submitButton, errorLabel])

Related

UnitTest does't work properly for MVP pattern Swift

I'm trying to write some UnitTests for the first time. My pattern is MVP and I'm trying to test my Presenter. I've created mock class: class TeamViewMock: TeamViewPresenterProtocol { }. It contains all the methods from my real Presenter. Inside the each method I'm trying to set the new value for the property, so when the method called - property should get a new value.
Only one property gets new value out of 4 and I've no clue why the other ones didn't get it.
You may see it in the following code
import XCTest
#testable import NHL
class TeamViewPresenterTest: XCTestCase {
var presenter: TeamViewPresenter!
var viewMock: TeamViewMock!
func setupPresenter() {
viewMock = TeamViewMock()
presenter = TeamViewPresenter(with: viewMock)
}
func testGetData() {
setupPresenter()
presenter.getData(completion: {_ in })
XCTAssertTrue(viewMock.isStart) // This one works and returns true
XCTAssertTrue(viewMock.isStop) // Return error
XCTAssertTrue(viewMock.isEndRefreshing) // Return error
XCTAssertTrue(viewMock.isReload) // Return error
}
}
class TeamViewMock: TeamViewPresenterProtocol {
var isStart = false
var isStop = false
var isEndRefreshing = false
var isReload = false
func startAnimating() {
self.isStart = true // Testing stops here and doesn't go any further...
}
func stopAnimating() {
self.isStop = true
}
func endRefreshing() {
self.isEndRefreshing = true
}
func reloadView(_ teams: NHLDTO) {
self.isReload = true
}
}
class TeamViewPresenter {
// MARK: - Public Properties
private weak var view: TeamViewPresenterProtocol?
public let dataFetcherService = DataFetcherService()
// MARK: - Initializers
init(with view: TeamViewPresenterProtocol) {
self.view = view
}
// MARK: - Public Methods
public func getData(completion: #escaping (AppError) -> Void) {
view?.startAnimating() // Testing stops here and doesn't go any further, but still returns true for the property isStart and error for the rest
dataFetcherService.fetchTeamData { [weak self] result in
guard let self = self else { return }
switch result {
case .failure(let error):
completion(error)
print(error)
case .success(let teams):
guard let teams = teams else { return }
self.view?.reloadView(teams)
self.view?.stopAnimating()
self.view?.endRefreshing()
}
}
}
}
protocol TeamViewPresenterProtocol: AnyObject {
func startAnimating()
func stopAnimating()
func reloadView(_ teams: NHLDTO)
func endRefreshing()
}

SwiftUI - Is it possible to change an ActionSheet button text after it is displayed?

I would like to show an ActionSheet containing InApp purchase objects the user can purchase.
But I want that sheet to contain the prices of such objects, like:
Object 1 ($1.99)
Object 2 ($2.99)
...
but the price is asynchronous, cause it has to be retrieved from the store.
So, I thought about doing this:
struct Package {
enum Packtype:String {
typealias RawValue = String
case obj1 = "com.example.object1"
case obj2 = "com.example.object2"
}
var productID:String = ""
#State var namePriceString:String = ""
init(productID:String) {
self.productID = productID
}
}
then, when I create the action sheet button I do this:
var obj1 = Package(productID: Package.Packtype.obj1.rawValue)
var obj2 = Package(productID: Package.Packtype.obj2.rawValue)
self.getPrices(packages:[obj1, obj2])
let obj1Button = ActionSheet.Button.default(Text(obj1.$namePriceString)) {
// do something with obj1
}
let obj2Button = ActionSheet.Button.default(Text(obj2.$namePriceString)) {
// do something with obj1
}
// build the actionsheet
later in the code:
func getPrices(packages:[Package]) {
let productIDS = Set(packages.map {$0.productID})
SwiftyStoreKit.retrieveProductsInfo(productIDS) { (answer) in
if answer.invalidProductIDs.first != nil { return }
let results = answer.retrievedProducts
if results.count == 0 { return }
for result in answer {
if let package = packages.filter({ ($0.productID == result.productIdentifier) }).first {
package.namePriceString = result.localizedTitle + "(" + "\(result.localizedPrice!)" + ")"
}
}
}
}
I have an error pointing to Text on the button creation lines saying
Initializer 'init(_:)' requires that 'Binding' conform to
'StringProtocol'
In a nutshell I need this:
I display the actionsheet. Its buttons contain no price.
I retrieve the prices
Actionsheet buttons are updated with the prices.
A possible solution is to return prices in a completion handler and only then display the action sheet:
struct ContentView: View {
#State var showActionSheet = false
#State var localizedPrices = [Package: String]()
var body: some View {
Button("Get prices") {
getPrices(packages: Package.allCases, completion: {
localizedPrices = $0
showActionSheet = true
})
}
.actionSheet(isPresented: $showActionSheet) {
let buttons = localizedPrices.map { package, localizedPrice in
ActionSheet.Button.default(Text(localizedPrice), action: { buy(package: package) })
}
return ActionSheet(title: Text("Title"), message: Text("Message"), buttons: buttons + [.cancel()])
}
}
}
func getPrices(packages: [Package], completion: #escaping ([Package: String]) -> Void) {
// simulates an asynchronous task, should be replaced with the actual implementation
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
let localizedPrices = Dictionary(uniqueKeysWithValues: packages.map { ($0, "\(Int.random(in: 1 ..< 100))") })
completion(localizedPrices)
}
}
func buy(package: Package) {
print("Buying \(package.rawValue)")
}
enum Package: String, CaseIterable {
case obj1 = "com.example.object1"
case obj2 = "com.example.object2"
}
This can be further tuned with loading animations etc...

delegate method does not get called second time

I am building a simple currency converter app. When ViewController gets opened it calls a function from CoinManager.swift:
class ViewController: UIViewController {
var coinManager = CoinManager()
override func viewDidLoad() {
super.viewDidLoad()
coinManager.delegate = self
coinManager.getCoinPrice(for: "AUD", "AZN", firstCall: true)
}
...
}
CoinManager.swift:
protocol CoinManagerDelegate {
func didUpdatePrice(price1: Double, currency1: String, price2: Double, currency2: String)
func tellTableView(descriptions: [String], symbols: [String])
func didFailWithError(error: Error)
}
struct CoinManager {
var delegate: CoinManagerDelegate?
let baseURL = "https://www.cbr-xml-daily.ru/daily_json.js"
func getCoinPrice (for currency1: String,_ currency2: String, firstCall: Bool) {
if let url = URL(string: baseURL) {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, response, error) in
if error != nil {
self.delegate?.didFailWithError(error: error!)
return
}
if let safeData = data {
if let coinData = self.parseJSON(safeData) {
if firstCall {
var descriptions = [""]
let listOfCoins = Array(coinData.keys)
for key in listOfCoins {
descriptions.append(coinData[key]!.Name)
}
descriptions.removeFirst()
self.delegate?.tellTableView(descriptions: descriptions, symbols: listOfCoins)
}
if let coinInfo1 = coinData[currency1] {
let value1 = coinInfo1.Value
if let coinInfo2 = coinData[currency2] {
let value2 = coinInfo2.Value
//this line does not do anything the second time I call getCoinPrice:
self.delegate?.didUpdatePrice(price1: value1, currency1: currency1, price2: value2, currency2: currency2)
//And this one does work
print("delegate:\(currency1)")
} else {
print("no name matches currency2")
}
} else {
print("no name matches currency1")
}
}
}
}
task.resume()
}
}
func ParseJSON....
}
The method it calls (ViewController.swift):
extension ViewController: CoinManagerDelegate {
func didUpdatePrice(price1: Double, currency1: String, price2: Double, currency2: String) {
print("didUpdatePrice called")
DispatchQueue.main.async {
let price1AsString = String(price1)
let price2AsString = String(price2)
self.leftTextField.text = price1AsString
self.rightTextField.text = price2AsString
self.leftLabel.text = currency1
self.rightLabel.text = currency2
}
}
...
}
and finally, CurrencyViewController.swift:
var coinManager = CoinManager()
#IBAction func backButtonPressed(_ sender: UIBarButtonItem) {
dismiss(animated: true, completion: nil)
coinManager.getCoinPrice(for: "USD", "AZN", firstCall: false)
}
So when I launch the app i get following in my debug console:
didUpdatePrice called
delegate:AUD
And when I call getCoinPrice() from CurrencyViewController the delegate method does not get called. I know that my code goes through the delegate function line as I get this in debug console:
delegate:USD
I just can't wrap my head around it. The delegate method does not work when gets called second time. Even though it is called by the same algorithm
It's because you're creating a new object of CoinManager in CurrencyViewController where the delegate is not set. So you've to set the delegate every time you create a new instance of CoinManager.
#IBAction func backButtonPressed(_ sender: UIBarButtonItem) {
dismiss(animated: true, completion: nil)
coinManager.delegate = self
coinManager.getCoinPrice(for: "USD", "AZN", firstCall: false)
}
Update: So, the above solution would require for you to make the delegate conformance in CurrencyViewController. If you're looking for an alternate solution you should probably pass the instance of coinManager in ViewController to CurrencyViewController. For that here are the things you need to update.
In CurrencyViewController:
class CurrencyViewController: UIViewController {
var coinManager: CoinManager! // you can optional unwrap if you intent to use CurrencyViewController without coinManager
//...
And in ViewController:
currencyViewController.coinManager = coinManager // passing the instance of coinManager
Can you share the full code of CoinManager? I see this part
if firstCall {
...
}
Maybe some block logic here or unhandled cases? And can you share the full code of protocol?
Also try to print something before this code:
if error != nil {
self.delegate?.didFailWithError(error: error!)
return
}

how to init() a swift view controller properly?

I'm attempting to initialize this ViewController class. I am not using the MVC design strategy so ignore the bad conventions used (if any).
How do I initialize this class properly?
Error: 'required' initializer 'init(coder:)' must be provided by subclass of 'UIViewController'
Context: This is a calculator app that when any of the buttons are pressed. It will go find the senders title and simply put if one of the three vars are nil, it will store it in that optional.
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
#IBOutlet weak var answerLabel: UILabel!
//global vars for all funcs
var selection1: Int? {
didSet { answerLabel.text = String(selection1!) }
}
var selection2: String? {
didSet { answerLabel.text = selection2! }
}
var selection3: Int? {
didSet { answerLabel.text = String(selection3!) }
}
var answer: Int {
didSet { answerLabel.text = String(answer) }
}
init() {
}
#IBAction func touchButton(_ sender: UIButton) {
if selection1 == nil {
selection1 = Int(sender.currentTitle!)
print("Selection set in first pos.")
} else if selection2 == nil {
selection2 = sender.currentTitle
} else if selection3 == nil {
selection3 = Int(sender.currentTitle!)
} else {
calculate(firstNum: selection1!, operation: selection2!, secondNum: selection3!)
}
}
func calculate(firstNum: Int, operation: String, secondNum: Int) {
switch operation {
case "+":
answer = firstNum + secondNum
case "-":
answer = firstNum - secondNum
case "x":
answer = firstNum * secondNum
case "/":
answer = firstNum / secondNum
default:
answerLabel.text = "Something went wrong!"
}
}
}
Initialization depends on a couple of condition.
If you are using storyboard, you can just remove the init and your VC will have default initializer. Make sure either all of your properties have default value or they are optional.
If you are using xib or just creating view programmatically you can have custom convenience initializer where you pass some extra data this way.
class MyViewController: ViewController {
var answer: Int
convenience init(answer: Int) {
self.init()
self.answer = answer
// Do other setup
}
}
Your controller is being instantiated from the storyboard. A safe place to configure initial views is during the controller's call to viewDidLoad, ie:
override func viewDidLoad() {
super.viewDidLoad()
// configure your views and subviews here
}

Button Click Twice

As we all know, to avoid clicking twice, we can set the code bellow on the tap method and add a HUD such as SVProgress.show().
isUserInteractionEnabled = false
After the network request, set it to true and SVProgress.dismiss().
I wonder if there is a method to extract the function for those button which needs to send a request. I have thought to use method swizzling. Add this to the button extension, the codes is bellow. It seems not good. Do you guys have some good ways to extract the function? Using inheritance, protocol or something else?
extension UIButton {
private struct AssociatedKeys {
static var cp_submitComplete = "cp_submitComplete"
static var cp_defaultMessage:String = NSLocalizedString("Loading", comment: "prompt")
static var cp_customMessage = "cp_customMessage"
}
var submitNotComplete: Bool {
get {
let objc_Get = objc_getAssociatedObject(self, &AssociatedKeys.cp_submitComplete)
if objc_Get != nil {
if let objc_Get = objc_Get as? Bool, objc_Get == true {
return true
}
return false
} else {
return false
}
}
set {
objc_setAssociatedObject(self, &AssociatedKeys.cp_submitComplete, newValue as Bool, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
if !newValue {
isUserInteractionEnabled = true
SVProgressHUD.dismiss()
}
}
}
var customMessage: String {
get {
let cp_customMessage = objc_getAssociatedObject(self, &AssociatedKeys.cp_customMessage)
if let message = cp_customMessage {
return message as! String
} else {
return AssociatedKeys.cp_defaultMessage
}
}
set {
objc_setAssociatedObject(self, &AssociatedKeys.cp_customMessage, newValue as String, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
override open class func initialize() {
if self == UIButton.self {
DispatchQueue.once(NSUUID().uuidString, block: {
let systemSel = #selector(UIButton.sendAction(_:to:for:))
let swizzSel = #selector(UIButton.cpSendAction(_:to:for:))
let systemMethod = class_getInstanceMethod(self, systemSel)
let swizzMethod = class_getInstanceMethod(self, swizzSel)
let isAdd = class_addMethod(self, systemSel, method_getImplementation(swizzMethod), method_getTypeEncoding(swizzMethod))
if isAdd {
class_replaceMethod(self, swizzSel, method_getImplementation(systemMethod), method_getTypeEncoding(systemMethod));
} else {
method_exchangeImplementations(systemMethod, swizzMethod);
}
})
}
}
private dynamic func cpSendAction(_ action: Selector, to target: Any?, for event: UIEvent?) {
cpSendAction(action, to: target, for: event)
if submitNotComplete {
//begin submit
isUserInteractionEnabled = false
SVProgressHUD.show(withStatus: customMessage)
}
}
}
I think it's a bad idea to handle this kind of logic in UIButton. I would rather make the view controller responsible for enabling/disabling the button.
func handleTap(_ sender: UIButton) {
sender.isEnabled = false
SVProgressHUD.show(withStatus: customMessage)
doSomeTaskAsync(withCompletion: {
sender.isEnabled = true
SVProgressHUD.dismiss()
})
}