I have a tabbed view app that I am working on right now I am wondering if there's any way I can share the information from my FirstViewController.swift to my SecondViewController.swift? Since I know swift doesnt support multiple class inheritance, is there a way that I can use the variables and informations I have on my FirstViewController in my SecondViewController?
You'll need a model outside the context of both your view controllers to store data, and then a way to populate those view controllers with that data and provide a reference to the model to make changes from the view controllers. You could this by creating a new model object that conforms to UITabBarColtronnerDelegate, which will allow you access to a view controller after it is selected. Keep things decoupled by abstracting these relationships into protocols, which then allows your implementations to vary independently as your app scales up.
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
let applicationModel = ApplicationModel()
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
if let tabBarController = window?.rootViewController as? UITabBarController {
tabBarController.delegate = applicationModel
}
return true
}
}
protocol TabbedViewControllerDelegate {
var message: String { set get }
}
class ApplicationModel: NSObject, UITabBarControllerDelegate, TabbedViewControllerDelegate {
var message = "Hello World!"
func tabBarController(tabBarController: UITabBarController, didSelectViewController viewController: UIViewController) {
if var tabbedViewController = viewController as? TabbedViewController {
tabbedViewController.message = message
tabBarController.delegate = self
}
}
}
protocol TabbedViewController {
var message: String { set get }
var delegate: TabbedViewControllerDelegate? { get set }
}
class FirstViewController: UIViewController, TabbedViewController {
var delegate: TabbedViewControllerDelegate?
var message: String = "" {
didSet {
println( "FirstViewController populated with message: \(message)" )
}
}
#IBAction func buttonPressed( sender: AnyObject? ) {
self.delegate?.message = "Updated message from FirstViewController"
}
}
class SecondViewController: UIViewController, TabbedViewController {
var delegate: TabbedViewControllerDelegate?
var message: String = "" {
didSet {
println( "SecondViewController populated with message: \(message)" )
}
}
#IBAction func buttonPressed( sender: AnyObject? ) {
self.delegate?.message = "Updated message from SecondViewController"
}
}
Related
This time, I am implementing the screen with RxSwift / MVVM.
It's too difficult to implement RxSwift as an MVVM.
What I wanted to ask you was to enter the list screen and get the data.
Then it went into the detail screen and changed specific data.
And if it go to the list screen, it have to update the data.
I think I can put data in the viewwillappear() of the view controller, and I do not know how to implement the renewal in the view Model, and I do not know if it is right to do this in functional programming like rx.
I defined the viewmodel as follows.
The store.getListEventWinning() method is a function that fits data and is delivered in the form of Observable.
Binding was done in view controller as below.
You don't give a lot of detail, despite the code you posted. I will address your specific comment, "What I wanted to ask you was to enter the list screen and get the data. Then it went into the detail screen and changed specific data. And if it go to the list screen, it have to update the data."
Using my CLE library (which you can install with cocoapods or SPM) doing this is quite simple.
let change = changeButton.rx.tap
.flatMapFirst(presentScene(animated: true) {
DetailViewController.scene { $0.connect() }
})
Here is a complete example that you can run yourself to see how it works. To run the below, just add XIB files for the two view controllers, and hook up the outlets.
import Cause_Logic_Effect
import RxCocoa
import RxSwift
final class MainViewController: UIViewController {
#IBOutlet var addButton: UIButton!
#IBOutlet var tableView: UITableView!
let disposeBag = DisposeBag()
}
final class DetailViewController: UIViewController {
#IBOutlet var saveButton: UIButton!
#IBOutlet var nameField: UITextField!
let disposeBag = DisposeBag()
}
extension MainViewController {
func connect() {
let initial = Observable<[String]>.just([]) // This could be a network request
let addName = addButton.rx.tap
.flatMapFirst(presentScene(animated: true) {
DetailViewController().scene { $0.connect() }
})
let state = mainVieweModel(initial: initial, addName: addName)
.share(replay: 1)
state
.bind(to: tableView.rx.items(cellIdentifier: "Cell", cellType: UITableViewCell.self)) { _, name, cell in
cell.textLabel?.text = name
}
.disposed(by: disposeBag)
}
}
func mainVieweModel(initial: Observable<[String]>, addName: Observable<String>) -> Observable<[String]> {
enum Input {
case initial([String])
case add(String)
}
return Observable.merge(
initial.map { Input.initial($0) },
addName.map { Input.add($0) }
)
.scan(into: [String]()) { state, input in
switch input {
case let .initial(value):
state = value
case let .add(text):
state.append(text)
}
}
}
extension DetailViewController {
func connect() -> Observable<String> {
return saveButton.rx.tap
.withLatestFrom(nameField.rx.text.orEmpty)
.take(1)
}
}
The app delegate looks like this:
import Cause_Logic_Effect
import UIKit
#main
final class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
window = {
let result = UIWindow(frame: UIScreen.main.bounds)
result.rootViewController = MainViewController().configure { $0.connect() }
result.makeKeyAndVisible()
return result
}()
return true
}
}
I've passed to my complicationDescriptors a userInfo dictionary that includes the names of my complications so I can be notified of which complication the user tapped on and launch them to that View. I'm using the new #App and #WKExtensionDelegateAdaptor for other reasons, so I have access to handleUserActivity in my extensionDelegate and can unpack the source of the complication tap, but how do I launch them to a specific view in SwiftUI? handleUserActivity seems to be set up to work with WKInterfaceControllers?
class ExtensionDelegate: NSObject, WKExtensionDelegate {
let defaults = UserDefaults.standard
func applicationDidFinishLaunching() {
//print("ApplicationDidFinishLanching called")
scheduleNextReload()
}
func applicationWillResignActive() {
//print("applicationWillResignActive called")
}
func handleUserActivity(_ userInfo: [AnyHashable : Any]?) {
if let complication = userInfo?[TrackerConstants.complicationUserTappedKey] as? String {
if complication == TrackerConstants.recoveryDescriptorKey {
//What now?
} else if complication == TrackerConstants.exertionDescriptorKey {
//What now?
}
}
}
I managed to update my view according to the tapped complication userInfo by using notifications, inspired by this answer.
First declare a notification name:
extension Notification.Name {
static let complicationTapped = Notification.Name("complicationTapped")
}
In your ExtensionDelegate:
func handleUserActivity(_ userInfo: [AnyHashable : Any]?) {
if let complication = userInfo?[TrackerConstants.complicationUserTappedKey] as? String {
NotificationCenter.default.post(
name: Notification.Name.complicationTapped,
object: complication
)
}
}
Finally in your view:
struct ContentView: View {
#State private var activeComplication: String? = nil
var body: some View {
NavigationView { // or TabView, etc
// Your content with NavigationLinks
}
.onReceive(NotificationCenter.default.publisher(
for: Notification.Name.complicationTapped
)) { output in
self.activeComplication = output.object as? String
}
}
}
For more information on how to activate a view from here, see Programmatic navigation in SwiftUI
I am trying to use MVVM. I am going to VC2 from VC1. I am updating the viewModel.fromVC = 1, but the value is not updating in the VC2.
Here is what I mean:
There is a viewModel, in it there is a var fromVC = Int(). Now, in vc1, I am calling the viewModel as
let viewModel = viewModel().
Now, on the tap of button, I am updating the viewModel.fromVC = 8. And, moving to the next screen. In the next screen, when I print fromVC then I get the value as 0 instead of 8.
This is how the VC2 looks like
class VC2 {
let viewModel = viewModel()
func abc() {
print(viewModel.fromVC)
}
}
Now, I am calling abc() in viewDidLoad and the fromVC is printed as 0 instead of 8. Any help?
For the MVVM pattern you need to understand that it's a layer split in 2 different parts: Inputs & Outputs.
Int terms of inputs, your viewModel needs to catch every event from the viewController, and for the Outputs, this is the way were the viewModel will send data (correctly formatted) to the viewController.
So basically, if we have a viewController like this:
final class HomeViewController: UIViewController {
// MARK: - Outlets
#IBOutlet private weak var titleLabel: UILabel!
// MARK: - View life cycle
override func viewDidLoad() {
super.viewDidLoad()
}
// MARK: - Actions
#IBAction func buttonTouchUp(_ sender: Any) {
titleLabel.text = "toto"
}
}
We need to extract the responsibilities to a viewModel, since the viewController is handling the touchUp event, and owning the data to bring to th label.
By Extracting this, you will keep the responsibility correctly decided and after all, you'll be able to test your viewModel correctly 🙌
So how to do it? Easy, let's take a look to our futur viewModel:
final class HomeViewModel {
// MARK: - Private properties
private let title: String
// MARK: - Initializer
init(title: String) {
self.title = title
}
// MARK: - Outputs
var titleText: ((String) -> Void)?
// MARK: - Inputs
func viewDidLoad() {
titleText?("")
}
func buttonDidPress() {
titleText?(title)
}
}
So now, by doing this, you are keeping safe the different responsibilities, let's see how to bind our viewModel to our previous viewController :
final class HomeViewController: UIViewController {
// MARK: - public var
var viewModel: HomeViewModel!
// MARK: - Outlets
#IBOutlet private weak var titleLabel: UILabel!
// MARK: - View life cycle
override func viewDidLoad() {
super.viewDidLoad()
bind(to: viewModel)
viewModel.viewDidLoad()
}
// MARK: - Private func
private func bind(to viewModel: HomeViewModel) {
viewModel.titleText = { [weak self] title in
self?.titleLabel.text = title
}
}
// MARK: - Actions
#IBAction func buttonTouchUp(_ sender: Any) {
viewModel.buttonDidPress()
}
}
So one thing is missing, you'll asking me "but how to initialise our viewModel inside the viewController?"
Basically you should once again extract responsibilities, you could have a Screens layer which would have the responsibility to create the view like this:
final class Screens {
// MARK: - Properties
private let storyboard = UIStoryboard(name: StoryboardName, bundle: Bundle(for: Screens.self))
// MARK: - Home View Controller
func createHomeViewController(with title: String) -> HomeViewController {
let viewModel = HomeViewModel(title: title)
let viewController = storyboard.instantiateViewController(withIdentifier: "Home") as! HomeViewController
viewController.viewModel = viewModel
return viewController
}
}
And finally do something like this:
let screens = Screens()
let homeViewController = screens.createHomeViewController(with: "Toto")
But the main subject was to bring the possibility to test it correctly, so how to do it? very easy!
import XCTest
#testable import mvvmApp
final class HomeViewModelTests: XCTestCase {
func testGivenAHomeViewModel_WhenViewDidLoad_titleLabelTextIsEmpty() {
let viewModel = HomeViewModel(title: "toto")
let expectation = self.expectation("Returned title")
viewModel.titleText = { title in
XCTAssertEqual(title, "")
expectation.fulfill()
}
viewModel.viewDidLoad()
waitForExpectations(timeout: 1.0, handler: nil)
}
func testGivenAHomeViewModel_WhenButtonDidPress_titleLabelTextIsCorrectlyReturned() {
let viewModel = HomeViewModel(title: "toto")
let expectation = self.expectation("Returned title")
var counter = 0
viewModel.titleText = { title in
if counter == 1 {
XCTAssertEqual(title, "toto")
expectation.fulfill()
}
counter += 1
}
viewModel.viewDidLoad()
viewModel.buttonDidPress()
waitForExpectations(timeout: 1.0, handler: nil)
}
}
And that's it 💪
I have this function in a helper class. It's to show/hide some stuff.
func showHideDetails(controller: UIViewController, isHidden: Bool) {
...
if controller is AddNewViewController {
let addNewViewController = controller as! AddNewViewController
addNewViewController.bgButton.isHidden = isHidden
} else if controller is EditViewController {
let editViewController = controller as! EditViewController
editViewController.bgButton.isHidden = isHidden
}
...
}
Is there is a way around to have one if statement, instead of one if statement for each controller? Something like,
if controller.hasProperty(bgButton) {
controller.bgButton.isHidden = isHidden
}
Thanks
You still need to cast using as? ..., however in order not to do that for all view controllers that have the bgButton, you can define a base protocol enforcing all classes conforming to it to have the bgButton:
public protocol Buttoned {
var bgButton: UIButton { get set }
func setHideButton(_ isHidden: Bool)
}
extension Buttoned {
public func setHideButton(_ isHidden: Bool) {
bgButton.isHidden = isHidden
}
}
public class AddNewViewController: Buttoned {
#IBOutlet fileprivate weak var bgButton: UIButton!
....
}
public class EditViewController: Buttoned {
#IBOutlet fileprivate weak var bgButton: UIButton!
....
}
then you can handle the action in the actual view controller like below:
func showHideDetails(controller: UIViewController, isHidden: Bool) {
...
if let controller = controller as? Buttoned {
controller.setHideButton(isHidden)
}
...
}
In the given scenario to replace your all if with single if means that you should have a common base class or your class conform to same protocol. However type cast still requires. You can use below code to achieve your desired functionality.
Create a protocol BackgroundButton
public protocol BackgroundButton {
var bgButton: UIButton { get }
}
Conforms all your custom UIViewController with this protocol in Extension like below
extension AddNewViewController : BackgroundButton {
var bgButton : UIButton {
return yourbutton // Use any instance of UIButton from your AddNewViewController
}
}
extension EditViewController : BackgroundButton {
var bgButton : UIButton {
return yourbutton // Use any instance of UIButton from your EditViewController
}
}
Finally update your method like this
func showHideDetails(controller: UIViewController, isHidden: Bool) {
...
if let controller = controller as? BackgroundButton {
controller.bgButton.isHidden = isHidden
controller.bgButton. //Do any thing which you want with your button
}
...
}
Hope this will help you to reduce your numbers of if
I am very new to Swift and programming in general.
I am trying to add a Pop Up Picker on a textfield and when the user selects the item from the picker, they can press OK with that item displayed in the textfield and the PopUp disappear.
I have successfully implemented this with a Pop Up Date Picker as I have used this from GutHub successfully. I thought it would be easy to mimic this code for my Pop Up Picker which has proven to be more difficult than expected.
I have a sepeate XIB file which holds the View with the Picker and OK Button. I then have 2 swift files one for the PopViewController and the other for the PopPicker.
Not even sure if this code is correct but the error I am getting is that my Picker does not conform to protocol. Code is below for both files.
PopEngineViewController
import UIKit
protocol EnginePickerViewControllerDelegate : class {
func enginePickerVCDismissed(string: UITextField?)
}
class PopEngineViewController: UIViewController {
#IBOutlet weak var container: UIView!
#IBOutlet weak var enginePicker: UIPickerView!
weak var delegate : EnginePickerViewControllerDelegate?
override convenience init() {
self.init(nibName: "PopEnginePicker", bundle: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func viewDidDisappear(animated: Bool) {
self.delegate?.enginePickerVCDismissed(nil)
}
}
and PopEnginePicker
import UIKit
public class PopEnginePicker : NSObject, UIPopoverPresentationControllerDelegate, EnginePickerViewControllerDelegate {
public typealias PopEnginePickerCallback = (forTextField : UITextField)->()
var enginePickerVC : PopEngineViewController
var popover : UIPopoverPresentationController?
var textField : UITextField!
var dataChanged : PopEnginePickerCallback?
var presented = false
var offset : CGFloat = 8.0
public init(forTextField: UITextField) {
enginePickerVC = PopEngineViewController()
self.textField = forTextField
super.init()
}
public func pick(inViewController : UIViewController, dataChanged : PopEnginePickerCallback) {
if presented {
return // we are busy
}
enginePickerVC.delegate = self
enginePickerVC.modalPresentationStyle = UIModalPresentationStyle.Popover
enginePickerVC.preferredContentSize = CGSizeMake(500,208)
popover = enginePickerVC.popoverPresentationController
if let _popover = popover {
_popover.sourceView = textField
_popover.sourceRect = CGRectMake(self.offset,textField.bounds.size.height,0,0)
_popover.delegate = self
self.dataChanged = dataChanged
inViewController.presentViewController(enginePickerVC, animated: true, completion: nil)
presented = true
}
}
func adaptivePresentationStyleForPresentationController(PC: UIPresentationController!) -> UIModalPresentationStyle {
return .None
}
}
Not even sure if I am going down the complete wrong path however I want it to look like the below as I have done with the date picker as it shows in the link below:
http://coding.tabasoft.it/ios/a-simple-ios8-popdatepicker/