I'm new to Swift and I'm trying to implement a custom UIKeyCommand architecture in a practice app. I wrote the extension below for the base UISplitViewController to show all UIKeyCommands in the current views on screen.
extension UISplitViewController {
open override var canBecomeFirstResponder: Bool {
return true
}
var BPKeyCommands: [BPKeyCommand]? {
var commands: [BPKeyCommand] = []
var mastervc = self.viewControllers.first
if (mastervc is UINavigationController) {
mastervc = (mastervc as! UINavigationController).viewControllers.last
}
if let masterCommands = mastervc.commands {
for command in masterCommands {
commands.append(command)
}
}
return commands
}
open override var keyCommands: [UIKeyCommand]? {
var commands: [UIKeyCommand] = []
if let bpkeycommands = BPKeyCommands {
for command in bpkeycommands {
let new = UIKeyCommand(input: command.input,
modifierFlags: command.modifierFlags,
action: #selector(executeKeyCommand(sender:)),
discoverabilityTitle: command.title)
commands.append(new)
}
}
return commands
}
#objc private func executeKeyCommand(sender: UIKeyCommand) {
if let index = keyCommands?.firstIndex(of: sender) {
if let command = BPKeyCommands?[index] {
command.action(command)
}
}
}
}
Now, as you might expect this throws an error at if let masterCommands = mastervc.commands {, because UIViewController doesn't contain the commands variable out of the box. My question is: how can I haveUIViewControllerhave that variable? Just like all controllers can overridekeyCommands` by default?
You have to create a protocol with command variable and make your view controller conform to it (step 1). Either you can provide values for particular view controller or you can provide a default implementation.
step 1:- Create a protocol with the variable you need.
protocol Commandable{
var commands: [String]{set get}
}
extension Commandable{
var commands: [String]{
get{return ["hello","world"]}
set{}
}
}
step 2:- Make then controllers which you are using conform it
step 3:- change the above code to get commands
if let commandProtocol = masterVC as? Commandable
{
let commands = commandProtocol.commands
}
else{
// handle it
}
Make sure the variable is unique so you don't accidentally override it.
Thank you.
you can create a extension of UIViewController and add that property on that extension of UIViewController. Then You will get it on child view controllers like UISplitViewController or any other custom ViewControllers. To know more about extensions, Which can be added on extension or what can be done by extension??
Related
I am trying to build an app with several screens. Every screen should have different buttons which all call one function.
My problem is that I do not understand how to call one function from different view controllers with input parameters.
Also I want to have another variable defined accessible and changeable from every view controller.
This is what I kind of want my code to be:
import UIKit
var address = "address"
public func makeRequest(Command: String){
let url = URL(address + Command)
print(url)
}
class MainViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let command = "command"
makeRequest(Command: command)
}
}
class SecondViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
print("address")
address = "address2"
}
}
If all classes are view controllers put the method in an extension of UIViewController and the address constant in a struct
struct Constant {
static let address = "address"
}
extension UIViewController {
public func makeRequest(command: String) -> URL? {
guard let url = URL(string: Constant.address + command) else { return nil }
print(url)
return url
}
}
extension UIViewController{
func getName(name: String)-> String{
print("hai \(name)")
return name
}
}
you can write a extension for Viewcontroller and try to call the method like this. if you write the extension for viewcontroller you can directly call them with its reference
self.getName(name:"Raghav")
I want to pass data between two view controllers, but don't want the view to change when the users presses my save data button.
The users needs to fill in multiple data fields, and when finish can press another button to go to the second view controller.
I found many tutorials how to pass data using segue, but they all change view as soon as the 'save button is pressed'.
Any one can explain to me how to alter the code?
#Phillip Mills: here is how I used your code. (what am I doing wrong?)
code:
//////// declaring classes on FirstViewController (trying it first on only one ViewController)
class FakeVC1 {
func userInput() {
DataModel.shared.username = outbj14u.text
}
class FakeVC2 {
func viewAppears() {
if let name = DataModel.shared.username {
outbj14p.text = name
print("I have nothing to say")
}
}
}
class DataModel {
static let shared = DataModel()
var username: String?
}
////till here
//// here is where i call the functions
override func viewDidAppear(_ animated: Bool) {
FakeVC1().userInput()
FakeVC2().viewAppears()
if let xbj14p = UserDefaults.standard.object(forKey: "outbj14p") as? String
{
outbj14p.text = xbj14p
}
if let xbj14u = UserDefaults.standard.object(forKey: "outbj14u") as? String
{
outbj14u.text = xbj14u
}
////
#Phillip Mills: Below is what I have know. I think I got the code on the FirstViewController right, but the code on the Second View controller must be wrong. I don't get any errors, but the text field on the SecondViewController remains unchanged after putting input on in the FirstViewController
//// Code on the FirstViewController
class DataModel {
static let shared = DataModel()
var username: String?
}
#IBAction func savebj14p(_ sender: Any) {
outbj14p.text = inbj14p.text
DataModel.shared.username = outbj14p.text
UserDefaults.standard.set(inbj14p.text, forKey: "namebj14p")
}
//and on the SecondViewController
#IBOutlet weak var bj14u: UILabel! // connected to a label
//and
class DataModel {
static let shared = DataModel()
var username: String?
}
override func viewDidAppear(_ animated: Bool) {
if let name = DataModel.shared.username {
bj14u.text = name
}
}
In your case, don't pass data.
Create a shared object to act as your data model. When users fill in the fields, update the data model.
When the user moves to the second controller/view, that controller uses the data model object to show what it needs to.
class FakeVC1 {
func userInput() {
DataModel.shared.username = "Me"
}
}
class FakeVC2 {
func viewAppears() {
if let name = DataModel.shared.username {
print(name)
} else {
print("I have nothing to say")
}
}
}
class DataModel {
static let shared = DataModel()
var username: String?
}
FakeVC1().userInput()
FakeVC2().viewAppears()
If you need to pass value to another viewcontroller without changing the view , you can user NSNotificationCenter class
Refer this link for more details
NSNotificationCenter addObserver in Swift
what i will recommend is to use a global variable or array, you will have the info in all view controllers and you will be able to call it in your new view controller.
This question already has answers here:
Passing data between view controllers
(45 answers)
Closed 5 years ago.
I'm trying to go back to my las viewController with sending data, but it doesn't work.
When I just use popViewController, I can go back to the page, but I can't move my datas from B to A.
Here is my code :
func goToLastViewController() {
let vc = self.navigationController?.viewControllers[4] as! OnaylarimTableViewController
vc.onayCode.userId = taskInfo.userId
vc.onayCode.systemCode = taskInfo.systemCode
self.navigationController?.popToViewController(vc, animated: true)
}
To pass data from Child to parent Controller, you have to pass data using Delegate pattern.
Steps to implement delegation pattern, Suppose A is Parent viewController and B is Child viewController.
Create protocol, and create delegate variable in B
Extend protocol in A
pass reference to B of A when Push or Present viewcontroller
Define delegate Method in A, receive action.
After that, According to your condition you can call delegate method from B.
You should do it using delegate protocol
class MyClass: NSUserNotificationCenterDelegate
The implementation will be like following:
func userDidSomeAction() {
//implementation
}
And ofcourse you have to implement delegete in your parent class like
childView.delegate = self
Check this for more information
https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Protocols.html
You have to send back to last ViewController with 2 options.
1. Unwind segue. (With use of storyboard)
You can refer this link.
2. Use of delegate/protocol.
You can refer this link.
Also this link will be useful for you.
You can use Coordinator Pattern
For example, I have 2 screens. The first displays information about the user, and from there, he goes to the screen for selecting his city. Information about the changed city should be displayed on the first screen.
final class CitiesViewController: UITableViewController {
// MARK: - Output -
var onCitySelected: ((City) -> Void)?
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
onCitySelected?(cities[indexPath.row])
}
...
}
UserEditViewController:
final class UserEditViewController: UIViewController, UpdateableWithUser {
// MARK: - Input -
var user: User? { didSet { updateView() } }
#IBOutlet private weak var userLabel: UILabel?
private func updateView() {
userLabel?.text = "User: \(user?.name ?? ""), \n"
+ "City: \(user?.city?.name ?? "")"
}
}
And Coordinator:
protocol UpdateableWithUser: class {
var user: User? { get set }
}
final class UserEditCoordinator {
// MARK: - Properties
private var user: User { didSet { updateInterfaces() } }
private weak var navigationController: UINavigationController?
// MARK: - Init
init(user: User, navigationController: UINavigationController) {
self.user = user
self.navigationController = navigationController
}
func start() {
showUserEditScreen()
}
// MARK: - Private implementation
private func showUserEditScreen() {
let controller = UIStoryboard.makeUserEditController()
controller.user = user
controller.onSelectCity = { [weak self] in
self?.showCitiesScreen()
}
navigationController?.pushViewController(controller, animated: false)
}
private func showCitiesScreen() {
let controller = UIStoryboard.makeCitiesController()
controller.onCitySelected = { [weak self] city in
self?.user.city = city
_ = self?.navigationController?.popViewController(animated: true)
}
navigationController?.pushViewController(controller, animated: true)
}
private func updateInterfaces() {
navigationController?.viewControllers.forEach {
($0 as? UpdateableWithUser)?.user = user
}
}
}
Then we just need to start coordinator:
coordinator = UserEditCoordinator(user: user, navigationController: navigationController)
coordinator.start()
Let's pretend we have an UITableViewController that on didSelectRowAtSection loads an instance of a class named i.e.: ClassToInject and it wants to inject it through a property injection because our ViewControllerToBePushed has a property of ClassToInject, that subsequently (because it's an UITabBarViewController) on the didSet callback it searches for all its viewControllers property that conforms to ClassToInjectPresentable simple as:
protocol ClassToInjectPresentable {
var property: ClassToInject { get set }
}
Until now, i would just do something like this:
func didSelectRowAtIndexPath {
let classToInject = self.loadClassToInjectFor(indexPath)
let tabBarViewController = SomeTabBarViewController()
tabBarViewController.property = classToInject
self.navigationController.push(tabBarViewController, animated: true)
}
And in SomeTabBarViewController ...
class SomeTabBarViewController: ClassToInjectPresentable {
var property: ClassToInject? {
didSet(newValue) {
self.viewControllers.filter{ $0 is ClassToInjectPresentable }.map{ $0 as! ClassToInjectPresentable }.forEach{ $0.property = newValue }
}
}
And everything should be get loaded nice and easy (but it's not). I've read about Swinject and this might be solved with it. I have seen lots of examples registering things like:
container.register(Animal.self) { _ in Cat(name: "Mimi") }
But I don't know if I can register some property that is loaded in self:
container.register(ClassToInjectInjector.self) { _ in
self.loadClassToInjectFor(indexPath) }
// And then
container.register(ClassToInjectPresentable.self) { _ in
SomeTabBarViewController() }
.initCompleted { r, p in
let tabBar = p as! SomeTabBarViewController
tabBar.property = r.resolve(ClassToInjectInjector.self)
// And lastly?
self.navigationController.pushViewController(tabBar, animated: true)
}
}
It is difficult to recommend proper solution without knowing details of your application, but here are some suggestions:
container.register(ClassToInjectInjector.self) { _ in
self.loadClassToInjectFor(indexPath)
}
In general, all register-ations should be done outside of your objects. Common setup ishaving one global Container, which contains all the registrations - you should look at them as instructions to build application objects without any implicit context. If your dependency needs to be created in the UITableViewController, you can pass it to resolve method as an argument:
container.register(ClassToInjectPresentable.self) { resolver, property in
let tabBar = SomeTabBarViewController()
tabBar.property = property
return tabBar
}
// in UItableVIewController
container.resolve(ClassToInjectPresentable.self,
argument: self.loadClassToInjectFor(indexPath))
Also this is usually a bad idea:
.initCompleted { r, p in
...
self.navigationController.pushViewController(tabBar, animated: true)
}
You should not mix application logic with DI - use Swinject purely for constructing your dependencies.
So your UITableViewController might look something like this:
func didSelectRowAtIndexPath {
let classToInject = self.loadClassToInjectFor(indexPath)
let tabBar = container.resolve(
SomeTabBarViewController.self, argument: loadClassToInjectFor(indexPath)
)
navigationController.push(tabBar, animated: true)
}
As for your TabBar and its view controllers: how do the UIViewControllers get into TabBar? Is it possible to do something like this?
class SomeTabBarViewController {
init(viewControllers: [UIViewController]) {
...
}
}
container.register(SomeTabBarViewController.self) { r, property
SomeTabBarViewController(viewControllers:[
r.resolve(MyViewController.self, argument: property),
r.resolve(MyViewController2.self, argument: property)
])
}
Finally I got the final answer by following the suggestions proposed.
public class Containers {
fileprivate init() { }
}
extension Containers {
static let activityPresentableContainer: Container = {
let container = Container()
container.register(ActivityTabBarController.self) { (r: Resolver, arg1: Activity) in
return ActivityTabBarController(activity: arg1)
}
container.register(ActivityPresentable.self) {
(r: Resolver, arg1: ActivityPresentableTabs, arg2: Activity) in
switch arg1 {
case .summary:
return ActivitySummaryViewController(activity: arg2)
case .detail:
return ActivityDetailPageViewController(activity: arg2)
case .map:
return ActivityMapViewController(activity: arg2)
case .charts:
return ActivityChartsViewController(activity: arg2)
case .strava:
return ActivityStravaViewController(activity: arg2)
}
}.inObjectScope(.transient)
return container
}()
With this approach, the named ActivityTabBarController gets instantiated always by the activityPresentableContainer using the following statement:
let controller = Containers.activityPresentableContainer.resolve(
ActivityTabBarController.self, argument: activity
)!
And then, each of the tabs inside the TabBarController gets instantiated using the required argument Activity and the type of tab itself using a .transient context. It resolves like this:
let activitySummary = Containers.activityPresentableContainer.resolve(
ActivityPresentable.self, arguments: ActivityPresentableTabs.summary, activity!
) as! UIViewController
This way I can generalize the tabs of the tab bar depending just on the information that they're using. If one of the tabs change in any moment, I can just change the registration, following the ActivityPresentable protocol.
I have two view controllers and I have this code to navigate between the two views
ViewController:
func goToSecondViewController() {
let aa = self.storyboard.instantiateViewControllerWithIdentifier("SecondViewController") as SecondViewController
self.navigationController.pushViewController(aa, animated: true)
}
/
SecondViewController:
func goToFirstViewController() {
self.navigationController.popToRootViewControllerAnimated(true)
}
How to send some data to the first view before popping it ?
If A is a view controller with the following declaration:
class A : UIViewController {
var data: String!
}
Then, whenever you have an instance of A, you can just set the data property directly:
let a = A() // assuming you've defined the init method
a.data = "hello"
Edit
If you want to send data before popping, you'd do something like:
func goToFirstViewController() {
let a = self.navigationController.viewControllers.first as! A
a.data = "data"
self.navigationController.popToRootViewControllerAnimated(true)
}
(haven't compiled the above)