When I switch from one screen to another, I get a black screen, I can't figure out why
Controller:
class AddContactsController: UIViewController {
// MARK: - Constants
private enum Constants {
static let textField = "nameTextCell"
static let datePicker = "datePicker"
static let pickerView = "pickerView"
static let textViewNotes = "textViewNotes"
static let alertOk = "OK"
static let alertQuestion = "It seems you made a mistake"
static let navigationTitle = "Create"
}
// MARK: - AddPresenter
var presenter: AddListPresenter?
func instantiate() -> UIViewController {
let vc = AddContactsController()
let presenter = AddListPresenter()
vc.presenter = presenter
return vc
}
Presenter:
class AddListPresenter {
weak var view: AddListController?
private var contact: Contact
init(contact: Contact? = nil) {
self.contact = contact ?? .init(
name: "",
surname: "",
middleName: "",
phone: "",
email: "",
date: "",
sex: "",
notes: ""
)
}
private var saveHieght: CGFloat = 0
How can this be avoided. I don't want to transfer presenter declarations to viewDidLoad
When you create a view controller by invoking it's initializer directly( e.g AddContactsController(), it's content view does not get created automatically like it would from a storyboad or a nibfile.
If you are creating you UI in code you need to show us your loadView() method. That's where you code to create your view hierarchy belongs.
Unless you have implemented loadView() you won't get any content view and will see a black screen exactly as you describe.
You also need to show the code that calls your instantiate() method and what you do with the view controller instance that's returned.
Related
In my SwiftUI project I have a button class model:
import SwiftUI
import Combine
class Button: Identifiable, ObservableObject {
var id = UUID()
#Published var title = String()
init(title: String) {
self.title = title
}
func changeTitle(title: String) {
self.title = title
}
}
Then I have another class named ControlPanel which has as a parameter a Button array.
class ControlPanel: Identifiable, ObservableObject {
var id = UUID()
#Published var name = String()
#Published var buttons:[Button] = []
init(name: String) {
self.name = name
}
}
I have to listen to this array in a custom collection view I have built with a UIViewControllerRepresentable class:
struct ButtonCollectionView: UIViewControllerRepresentable {
#Binding var buttons: [Button]
func makeUIViewController(context: Context) -> UICollectionViewController {
let vc = CollectionViewController(collectionViewLayout: .init())
vc.buttonArray = buttons
return vc
}
func updateUIViewController(_ uiViewController: UICollectionViewController, context: Context) {
if let vc = uiViewController as? CollectionViewController {
vc.buttonArray = buttons
vc.collectionView.reloadData()
}
}
}
Lastly, I call for this collection view in my content view, the following way:
#ObservedObject var controlPanel: ControlPanel = ControlPanel(name: "test")
var body: some View {
ButtonCollectionView(button: $controlPanel.buttons)
}
When I load my content view I do get all the cells, but once I change them, the view won't get updated. How can I fix this issue?
Your Button is already inside another #Published property and thus #Published var title won't work.
The simplest solution is to make your Button a struct (and rename it to something not used by SwiftUI, eg. CustomButton):
struct CustomButton: Identifiable {
var id = UUID()
var title = ""
mutating func changeTitle(title: String) {
self.title = title
}
}
For more advanced solutions see: How to tell SwiftUI views to bind to nested ObservableObjects
How can I access a title inside the navigation controller from a separate Class? I embedded my mainVC into the navigation controller via Storyboard. I can access the title from inside the viewDidLoad from inside the same Class like this self.navigationItem.title = "MyTitle". However, I need to access the title from a separate class 'CustomNavigation like this:
class: CustomNavigation: UIViewController() {
override func viewDidLoad(){
super.viewDidLoad()
func createCustomNav(){
self.navigationItem.title = "MyTitle"
}
}
}
Unfortunately it doesn't work. I also tried this:
func createCustomNav(){
let nav = UINavigationBar()
let title = UINavigationItem(title: "MyTitle")
nav.setItems([title], animated: false)
self.view.addSubview(nav)
}
This does't work neither. I don't get any errors.
I instantiate CustomNavigation inside the mainVC: var customNavigation = CustomNavigation()
Any help would be greatly appreciate it!
Set up a protocol that describes the work you want done.
protocol NavigationTitler {
func updateTitle(with title: String)
}
Define how your class will use the protocol.
class CustomNavigation: UINavigationController {
var titleDelegate: NavigationTitler?
func work() {
if let del = titleDelegate {
del.updateTitle(with: "Title from other object")
}
}
}
Implement the protocol (this is for your mainVC).
class ViewController: UIViewController, NavigationTitler {
var customNavigation: CustomNavigation?
func updateTitle(with title: String) {
self.navigationItem.title = title
}
func buildCustomController() {
customNavigation = CustomNavigation()
customNavigation!.titleDelegate = self
}
}
Get the title from a UITabBarItem, use as title in current VC:
//Titel einstellen
let x = navigationController?.tabBarItem.title ?? ""
self.title = x //titel in VC
I'm testing the Combine framework and using BindableObject as a notification hub for passing data among several views in a SwiftUI ContentView.
One of the views is a table. I click on a row and the value is detected in the print checkpoint, so the bindableobject receives the update.
Problem is, the new string is not broadcasted to the receiving end on the ContentView.
I'm new to this.
View controller with a table view .swift (broadcaster):
import SwiftUI
import Combine
final public class NewestString: BindableObject {
public var didChange = PassthroughSubject<NewestString, Never>()
var newstring: String {
didSet {
didChange.send(self)
print("Newstring: \(newstring)") //<-- Change detected
}
}
init(newstring: String) {
self.newstring = newstring
}
public func update() {
didChange.send(self)
print("--Newstring: \(newstring)")
}
}
final class AViewController: UIViewController {
#IBOutlet weak var someTableView: UITableView!
var returnData = NewestString(newstring:"--")
override func viewDidLoad() {
super.viewDidLoad()
}
}
/// [.....] More extensions here
extension AViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let completion = someResults[indexPath.row]
//// [......] More code here
self.returnData.newstring = "Test string" //<--- change caused
}
}
}
Main content View (broadcast destination):
import SwiftUI
import Combine
struct PrimaryButton: View {
var title: String = "DefaultTitle"
var body: some View {
Button(action: { print("tapped") }) {
Text(title)
}
}
}
struct MyMiniView: View {
#State var aTitle: String = "InitialView"
var body: some View {
VStack{
PrimaryButton(title: aTitle)
}
}
}
struct ContentView: View {
#State private var selection = 0
#ObjectBinding var desiredString: NewestString = NewestString(newstring: "Elegir destino") // <-- Expected receiver
var body: some View {
TabbedView(selection: $selection){
ZStack() {
MyMiniView(aTitle: self.desiredString.newstring ?? "--")
// expected end use of the change, that never happens
[...]
}
struct AView: UIViewControllerRepresentable {
typealias UIViewControllerType = AViewController
func makeUIViewController(context: UIViewControllerRepresentableContext<AView>) -> AViewController {
return UIStoryboard(name: "MyStoryboard", bundle: nil).instantiateViewController(identifier: String(describing: AViewController.self)) as! AViewController
}
func updateUIViewController(_ uiViewController: AViewController, context: UIViewControllerRepresentableContext<AView>) {
//
}
It compiles, runs and prints the change, but no update happens to the MyMiniView's PrimaryButton.
I can't find where you are using your instance of AViewController, but the issue comes from the fact that you are using multiple instance of your bindable object NewestString.
The ContentView as an instance of NewestString, which every update will trigger a view reload.
struct ContentView: View {
#State private var selection = 0
// First instance is here
#ObjectBinding var desiredString: NewestString = NewestString(newstring: "Elegir destino") // <-- Expected receiver
}
The second instance of NewestString is in AViewController, which you actually modify. But, as it's not the same instance of NewestString (the one that is actually declared in the content view), modifying it doesn't trigger the view reload.
final class AViewController: UIViewController {
#IBOutlet weak var someTableView: UITableView!
// The second instance is here
var returnData = NewestString(newstring:"--")
}
To solve this, you need to find a way to "forward" the instance of NewestString created inside your your ContentView to the view controller.
Edit: Found a way to pass the instance of the ObjectBinding to the view controller:
When you add your view into the hierarchy using SwiftUI, you need to pass a Binding of the value that you want to access from the view controller:
struct ContentView: View {
#ObjectBinding var desiredString = NewestString(newstring: "Hello")
var body: some View {
VStack {
AView(binding: desiredString[\.newstring])
Text(desiredString.newstring)
}
}
}
The subscript with a key path will produce a Binding of the given property:
protocol BindableObject {
subscript<T>(keyPath: ReferenceWritableKeyPath<Self, T>) -> > Binding<T> { get }
}
In the view controller wrapper (UIViewControllerRepresentable), you need to forward the given Binding to the actual view controller instance.
struct AView: UIViewControllerRepresentable {
typealias UIViewControllerType = AViewController
var binding: Binding<String>
func makeUIViewController(context: UIViewControllerRepresentableContext<AView>) -> AViewController {
let controller = AViewController()
controller.stringBinding = binding // forward the binding object
return controller
}
func updateUIViewController(_ uiViewController: AViewController, context: UIViewControllerRepresentableContext<AView>) {
}
}
And then, in you view controller, you can use the binding to update your value (using the .value property):
final class AViewController: UIViewController {
var stringBinding: Binding<String>!
override func viewDidLoad() {
super.viewDidLoad()
stringBinding.value = "Hello world !!"
}
}
When the view controller's viewDidLoad is called, the desiredString (in ContentView) will be updated to "Hello world !!", just like the displayed text (Text(desiredString.newstring)).
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'm writing a UserNotification library to reuse in many apps. The below .swift file is just dragged into a new project when needed. I'm then adding an NSObject to the View Controller Scene, setting it's custom class to KTUserNotification. I then drag a referencing outlet from this NSObject into the view controller. From there I use this outlet to call Show() to display notifications.
I use the NSObject with a custom class approach because the instance needs to stay allocated until the app ends. I first tried making it just a custom class, using let usernotification = KTUserNotification(). This however deallocated before the call came through to the "shouldPresent notification", causing a EXC_BAD_ACCESS. The #IBInspectables were just a nice addition I tossed in when moving to the NSObject approach.
Is there another way of keeping the instance allocated until the program ends without using an NSObject? Os is the NSObject the best way to go here?
import Cocoa
class KTUserNotification: NSObject, NSUserNotificationCenterDelegate {
#IBInspectable var showEvenIfAppIsFocus: Bool = true
#IBInspectable var title: String = ""
#IBInspectable var subTitle: String = ""
#IBInspectable var informativeText: String = ""
#IBAction func showNotification(_ sender: Any) {
Show()
}
override func awakeFromNib() {
NSUserNotificationCenter.default.delegate = self
}
func userNotificationCenter(_ center: NSUserNotificationCenter, shouldPresent notification: NSUserNotification) -> Bool {
return showEvenIfAppIsFocus
}
public func Show(title : String, subTitle: String, informativeText : String) {
let notification = NSUserNotification()
let randIdentifier = Int(arc4random_uniform(999999999) + 1)
notification.identifier = "KTUserNotification\(randIdentifier)"
notification.title = title
notification.subtitle = subTitle
notification.informativeText = informativeText
notification.soundName = NSUserNotificationDefaultSoundName
let notificationCenter = NSUserNotificationCenter.default
notificationCenter.deliver(notification)
}
public func Show() {
Show(title: title, subTitle: subTitle, informativeText: informativeText)
}
}