I have a class which is inherited from BaseViewController.swift. I have defined a flag for core data changes tracking:
var coreDataUpdated: Bool?
I've also added an observer for core data changes in viewWillAppear of BaseViewController.swift
//Core data update
NotificationCenter.default.addObserver(self,
selector: #selector(self.CoreDataUpadated),
name: .NSManagedObjectContextObjectsDidChange,
object: nil)
Now whenever I notified about changes in core data, I change coreDataUpdated variable to true
#objc func CoreDataUpadated() {
self.coreDataUpdated = true
}
Now in my ChildViewController.swift in viewDidAppear when I check for coreDataUpdated it returns me nil
here is my complete code:
class BaseViewController: UIViewController {
var coreDataUpdated: Bool?
override func viewWillAppear(_ animated: Bool) {
//Core data update
NotificationCenter.default.addObserver(self,
selector: #selector(self.CoreDataUpadated),
name: .NSManagedObjectContextObjectsDidChange,
object: nil)
}
func CoreDataUpadated() {
self.coreDataUpdated = true
}
}
class ChildViewController: BaseViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
reloadData()
}
func reloadData() {
if super.coreDataUpdated ?? false { //I always get nil for
super.coreDataUpdated
tableView?.reloadData()
}
}
}
Notice: I'm working on Xcode 9 beta 5 and iOS 11 with swift 4. However this code works fine on the Xcode 8 and iOS 10.3 with swift 3.
Class inheritance does not mean that subclass instances inherit the values assigned to properties in superclass instances.
You have an instance of BaseViewController which has a coreDataUpdated property and instance of ChildViewController, which has a coreDataUpdated property since it inherits from BaseViewController, but it's property value is unrelated to the BaseViewController instance property value; they are different objects.
When you say self.coreDataUpdated = true you are setting the property on the BaseViewController instance.
When you say super.coreDataUpdated you are referring to the coreDataUpdated property in the ChildViewController instance that is defined by it's superclass; since this property has never been assigned a value it is nil.
Since ChildViewController does not override coreDataUpdated super.coreDataUpdated is the same as self.coreDataUpdated in an instance of ChildViewController, so you could re-write that if statement as:
func reloadData() {
if self.coreDataUpdated ?? false {
tableView?.reloadData()
}
}
Hopefully this makes it clearer as to why coreDataUpdated is nil
Rather than using your own Bool, it may be simpler to examine the hasChanges property of your NSManagedObjectContext instance.
Thanks to #Paulw11 comments, I found out with inheritance the value won't be shared. So I use KVC pattern as the solution. this is my code after change
enum coreDataState: String {
case inserted = "inserted"
case deleted = "deleted"
case updated = "updated"
var description: String {
switch self {
case .inserted: return "inserted"
case .deleted: return "deleted"
case .updated: return "updated"
}
}
}
class BaseViewController: UIViewController {
override func viewWillAppear(_ animated: Bool) {
//Core data update
NotificationCenter.default.addObserver(self,
selector: #selector(self.CoreDataUpadated),
name: .NSManagedObjectContextObjectsDidChange,
object: nil)
}
func CoreDataUpdated(_ notification: Notification) {
if let userDictionary = notification.userInfo as? Dictionary<String, Any> {
for KV in userDictionary {
if KV.key != "managedObjectContext" {
NSUserDefaultManager.SaveItem(KV.key, key: coreDataUpdateKey)
}
}
}
}
func CoreDataChanged() -> coreDataState? {
if let currentState = NSUserDefaultManager.LoadItem(coreDataUpdateKey) as? String {
NSUserDefaultManager.RemoveItem(coreDataUpdateKey)
return coreDataState(rawValue: currentState)
}
return nil
}
}
class ChildViewController: BaseViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
reloadData()
}
func reloadData() {
if super.CoreDataChanged() != .deleted {
loadData()
tableView?.reloadData()
}
}
}
Although it is possible to implement it with other patterns such as delegate or override the CoreDataUpdated function in child classes
Related
Im having button in all viewcontrollers to change language
LanguageViewController.swift
class LanguageViewController: UIViewController {
#IBAction func actionChange(_ sender: Any) {
L102Language.currentAppleLanguage()
L102Language.setAppleLAnguageTo(lang: "en")
// below code to refresh storyboard
self.viewDidLoad()
}
}
L102Language.swift
class func currentAppleLanguage() -> String{
let userdef = UserDefaults.standard
let langArray = userdef.object(forKey: APPLE_LANGUAGE_KEY) as! NSArray
let current = langArray.firstObject as! String
let endIndex = current.startIndex
let currentWithoutLocale = current.substring(to: current.index(endIndex, offsetBy: 2))
return currentWithoutLocale
}
/// set #lang to be the first in Applelanguages list
class func setAppleLAnguageTo(lang: String) {
let userdef = UserDefaults.standard
userdef.set([lang,currentAppleLanguage()], forKey: APPLE_LANGUAGE_KEY)
userdef.synchronize()
}
I inherited LanguageViewController in all my FirstViewCOntroller, SecondController as below
class FirstViewController: LanguageViewController {
}
class SecondController: LanguageViewController {
}
If I call self.viewDidLoad() it fails to change language from view defined in storyboard. How to reload storyboard, so that the language should change in all viewcontroller,if any button from any viewcontroller is clicked? Thanks!
You can use NotificationCenter for reloading the view controllers content, this will also reload the content of view controllers that are not visible.
extension Notification.Name {
static let didChangeLanguage = Notification.Name("didChangeLanguage")
}
override func viewDidLoad() {
//Add a listener
NotificationCenter.default.addObserver(self, selector: #selector(onDidChangeLanguage(_:)), name: .didChangeLanguage, object: nil)
}
#IBAction func actionChange(_ sender: Any) {
L102Language.currentAppleLanguage()
L102Language.setAppleLAnguageTo(lang: "en")
// Notify about the change.
NotificationCenter.default.post(name: .didChangeLanguage, object: self, userInfo: nil)
}
#objc func onDidChangeLanguage(_ notification:Notification) {
// reload content using selected language.
}
Correct me if I'm wrong. but I think you don't need to reload all view controllers. you just need to update them when they get displayed, view controllers are behind the presented one are not visible for the user.
for doing that you can do something like this:
var currentLanguage = ""
override func viewDidLoad() {
currentLanguage = currentAppleLanguage()
loadContentForLanguage(currentLanguage)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// this will be executed every time this sceen gets display
if currentLanguage != currentAppleLanguage() {
currentLanguage = currentAppleLanguage()
loadContentForLanguage(currentLanguage)
}
}
func loadContentForLanguage(_ currentLanguage: String) {
//here it goes whatever you currently have in viewDidLoad
}
My apologies if this does not compile, my swift is really rusty.
I am trying to pass data from a firstVC to a second VC I have tried using delegate but it never worked (did not show required response) so I tried callback too and it now working so I am pasting both lines of code so any help is welcomed
Delegate:
protocol RatingDelegate: class {
func didLoadRating(ratings : [RatingModel])
}
the viewcontroller which the data would be passed from
ViewController A:
var delegate : RatingDelegate?
func showRatings(ratings: [RatingModel]) {
if delegate != nil {
delegate?.didLoadRating(ratings: ratings)
}
}
where the delegate value is supposed to me printed
RatingVC:
extension RatingVC: RatingDelegate {
func didLoadRating(ratings: [RatingModel]) {
log(ratings)
}
}
The callback Version
The view controller that would get the data
var ratingsCallBack: (() -> ([RatingModel]))?
the view controller which the value would be passed from
func showRatings(ratings: [RatingModel]) {
let ratingVC = RatingVC()
ratingVC.ratingsCallBack!() = {[unowned self] in
return ratings
}
}
this how ever throws a response saying
Expression is not assignable: function call returns immutable value
So the FirstVC passes data to RatingVC.
On FirstVC, at the point were you invoke RatingVC you should assign the delegate.
let ratingVC = RatingVC()
self.delegate = ratingVC //Here you specify RatingVC is the delegate variable
self.present(ratingVC, animated: true)
also
if delegate != nil {
}
is unnecessary, just do delegate?.didLoadRating(ratings: ratings) to keep it cleaner
EDIT: For the callback version is the same, just assign the value to the callback before initializing the view controller that sends the data.
It looks strange:
var ratingsCallBack: (() -> ([RatingModel]))?
should be something like this:
var ratingsCallBack: (([RatingModel]) -> ())?
so in case with callback:
class A: UIViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let ratingVC = RatingVC()
ratingVC.ratingsCallBack = { arr in
arr.forEach({ (model) in
print(model.rating)
})
}
navigationController?.pushViewController(ratingVC, animated: false)
}
}
class RatingVC: UIViewController {
var ratingsCallBack: (([RatingModel]) -> ())?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
}
#IBAction private func someButtonAction(_ sender: Any) {
let arr = [RatingModel.init(rating: 5), RatingModel()]
ratingsCallBack?(arr)
}
}
struct RatingModel {
var rating: Int = 1
}
Then when you press "someButton" you get this array in controller "A"
I want to receive the same callback in the ViewController that is opened at in the time that server response in my Swift Application.
I have two ViewControllers. The first ViewController registers a callBack from a class "NetworkService".
The second ViewController is Opened from the first ViewController and the second receives the "NetworkService" from the firstViewController initialized in a variable, and then registers the same callBack.
When I try to receive the callback from the server, if the first ViewController is opened I get the response. If I open the second ViewController and I resend the response I get this correctly in the second ViewController.
BUT if I return to the first ViewController and I get the response, its' only received on the Second ViewController all times.
class NetworkService {
var onFunction: ((_ result: String)->())?
func doCall() {
self.onFunction?("result")
}
}
class MyViewController: UIViewController {
let networkService = NetworkService()
override func viewDidLoad() {
super.viewDidLoad()
networkService.onFunction = { result in
print("I got \(result) from the server!")
}
}
}
I open the secondViewController like:
let vc = self.storyboard!.instantiateViewController(withIdentifier: "second") as! SecondViewController
vc. networkService = networkService
self.navigationController?.pushViewController(vc, animated: true)
And the Second ViewController:
class SecondViewController: UIViewController {
var networkService: NetworkService?
override func viewDidLoad() {
super.viewDidLoad()
networkService!.onFunction = { result in
print("I got \(result) from the server!")
}
}
}
How would it be possible to receive the response in the first ViewController again, then return to first ViewController from the second calling the popViewController?
self.navigationController?.popViewController(animated: false)
How about calling the function within viewDidAppear on both ViewControllers so that you get your response every time you switch between the two views? You wouldn't need to pass networkService between the ViewControllers.
override func viewDidAppear(_ animated: Bool) {
networkService!.onFunction = { result in
print("I got \(result) from the server!")
}
}
You can use notification but you will have to register and deregister VC as you switch between views. Other option is to use delegate, you will need to share NetworkService instance. Quick example of how this could work with protocol.
protocol NetworkServiceProtocol {
var service: NetworkService? { get }
func onFunction(_ result: String)
}
class NetworkService {
var delegate: NetworkServiceProtocol?
func doCall() {
self.delegate?.onFunction("results")
}
func update(delegate: NetworkServiceProtocol) {
self.delegate = delegate
}
}
class VC1: UIViewController, NetworkServiceProtocol {
var service: NetworkService?
init(service: NetworkService? = nil) {
self.service = service
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.service?.update(delegate: self)
}
func onFunction(_ result: String) {
print("On Function")
}
}
I am having issues trying to pass the data back to the ViewController (from BarCodeScannerViewController to TableViewController)
SecondVC (BarCodeScannerViewController.swift):
#objc func SendDataBack(_ button:UIBarButtonItem!) {
if let presenter = self.presentingViewController as? TableViewController {
presenter.BarCode = "Test"
}
self.dismiss(animated: true, completion: nil)
}
FirstVC (TableViewController.swift):
// The result is (BarCode - )
var BarCode: String = ""
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
print("BarCode - \(BarCode)")
}
Each time ViewWillAppear is running the value is not set, what could be causing this issue?
You should use the delegate pattern. I doubt in your code above that self.presentingViewController is actually set.
An example of using the delegate pattern for this:
// BarCodeScannerViewController.swift
protocol BarcodeScanningDelegate {
func didScan(barcode: String)
}
class BarCodeScannerViewController: UIViewController {
delegate: BarcodeScanningDelegate?
#objc func SendDataBack(_ button:UIBarButtonItem!) {
delegate?.didScan(barcode: "Test")
}
}
// TableViewController
#IBAction func scanBarcode() {
let vc = BarCodeScannerViewController()
vc.delegate = self
self.present(vc, animated: true)
}
extension TableViewController: BarcodeScanningDelegate {
func didScan(barcode: String) {
print("[DEBUG] - Barcode scanned: \(barcode)")
}
}
I found Router in Clean Swift architecture is responsible to navigate and pass data between view controllers. Some samples and articles depict that Routers use segue to communicate with view controllers. What would be the convenient design when I don't want to use any segue from Storyboard. Is it possible to pass data without segue in Clean Swift? If you describe with simplest complete example, would be appreciated.
Article says that you can:
// 2. Present another view controller programmatically
You can use this to manually create, configure and push viewController.
Example.
Let's pretend that you have ViewController with button (handle push):
final class ViewController: UIViewController {
private var router: ViewControllerRouterInput!
override func viewDidLoad() {
super.viewDidLoad()
router = ViewControllerRouter(viewController: self)
}
#IBAction func pushController(_ sender: UIButton) {
router.navigateToPushedViewController(value: 1)
}
}
This ViewController has router that implements ViewControllerRouterInput protocol.
protocol ViewControllerRouterInput {
func navigateToPushedViewController(value: Int)
}
final class ViewControllerRouter: ViewControllerRouterInput {
weak var viewController: ViewController?
init(viewController: ViewController) {
self.viewController = viewController
}
// MARK: - ViewControllerRouterInput
func navigateToPushedViewController(value: Int) {
let pushedViewController = PushedViewController.instantiate()
pushedViewController.configure(viewModel: PushedViewModel(value: value))
viewController?.navigationController?.pushViewController(pushedViewController, animated: true)
}
}
The navigateToPushedViewController func can takes any parameter you want (it is good to encapsulate parameters before configure new vc, so you may want to do that).
And the PushedViewController hasn't any specific implementation. Just configure() method and assert (notify you about missing configure() call):
final class PushedViewModel {
let value: Int
init(value: Int) {
self.value = value
}
}
final class PushedViewController: UIViewController, StoryboardBased {
#IBOutlet weak var label: UILabel!
private var viewModel: PushedViewModel!
func configure(viewModel: PushedViewModel) {
self.viewModel = viewModel
}
override func viewDidLoad() {
super.viewDidLoad()
assert(viewModel != nil, "viewModel is nil. You should call configure method before push vc.")
label.text = "Pushed View Controller with value: \(viewModel.value)"
}
}
Note: also, i used Reusable pod to reduce boilerplate code.
Result:
As above article explained you can use option 2/3/4 of navigateToSomewhere method as per your app design.
func navigateToSomewhere()
{
// 2. Present another view controller programmatically
// viewController.presentViewController(someWhereViewController, animated: true, completion: nil)
// 3. Ask the navigation controller to push another view controller onto the stack
// viewController.navigationController?.pushViewController(someWhereViewController, animated: true)
// 4. Present a view controller from a different storyboard
// let storyboard = UIStoryboard(name: "OtherThanMain", bundle: nil)
// let someWhereViewController = storyboard.instantiateInitialViewController() as! SomeWhereViewController
// viewController.navigationController?.pushViewController(someWhereViewController, animated: true)
}
You need pass data across protocols
protocol SecondModuleInput {
// pass data func or variable
var data: Any? { get set }
}
protocol SecondModuleOutput {
// pass data func or variable
func send(data: Any)
}
First presenter
class FirstPresenter: SecondModuleOutput {
var view: UIViewController
var secondModuleInputHandler: SecondModuleInput?
// MARK: SecondModuleInput
func send(data: Any) {
//sended data from SecondPresenter
}
}
Second presenter
class SecondPresenter: SecondModuleInput {
var view: UIViewController
var secondModuleOutputHandler: SecondModuleOutput?
static func configureWith(block: #escaping (SecondModuleInput) -> (SecondModuleOutput)) -> UIViewController {
let secondPresenter = SecondPresenter()
secondPresenter.secondModuleOutputHandler = block(secondPresenter)
return secondPresenter.view
}
// Sending data to first presenter
func sendDataToFirstPresenter(data: Any) {
secondModuleOutputHandler?.send(data: data)
}
// MARK: FirstModuleInput
var data: Any?
}
Router
class FirstRouter {
func goToSecondModuleFrom(firstPresenter: FirstPresenter, with data: Any) {
let secondPresenterView = SecondPresenter.configureWith { (secondPreseter) -> (SecondModuleOutput) in
firstPresenter.secondModuleInputHandler = secondPreseter
return firstPresenter
}
//Pass data to SecondPresenter
firstPresenter.secondModuleInputHandler?.data = data
//Go to another view controller
//firstPresenter.view.present(secondPresenterView, animated: true, completion: nil)
//firstPresenter.view.navigationController.pushViewController(secondPresenterView, animated: true)
}
}