UIView contents not updating in UIKit Swift - ios

I have a UIView which I am passing data from another view using getData(data: MydataModel). The data arrives at the second view but with a delay.
Here my sample code:
class MyView: UIView {
var myData: MydataModel?
init() {
loadView()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
func getData(data: MydataModel) {
self.myData = data
}
func loadView() {
label.text = myData?.city
}
}
I tried using property observer but I run into issues and not updating content inside the view.

The problem is that calling getData(data:) only sets the myData variable - it doesn't update any visible content in the view.
You can add a property observer to myData that calls loadView to update the view:
var myData: MydataModel? {
didSet {
loadView()
}
}
If you're still running into issues, make sure that all view updates are happening on the main thread, so perhaps you need call getData(data:) within DispatchQueue.main.async.

Related

IBAction for UIButton with it's own .xib file

I want to create a reusable button all over my app and was planning to design it with it's own .xib file. The issue is that I can't connect an IBAction to the custom button in the controllers where it's used.
I created a new .xib file called SampleButton.xib and added a button. This is what the hierarchy and the view looks like:
I then created a new swift file called SampleButton.swift with a class called SampleButton that's a subclass of UIButton and assigned it as the File's Owner in my SampleButton.xib file.
The contents of SampleButton.swift are as follows:
import Foundation
import UIKit
#IBDesignable
class SampleButton: UIButton {
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
func setup() {
guard let view = loadViewFromNib() as? UIButton else {
return
}
view.frame = bounds
view.autoresizingMask = [UIView.AutoresizingMask.flexibleWidth,
UIView.AutoresizingMask.flexibleHeight]
addSubview(view)
view.layer.borderWidth = 2
view.layer.borderColor = UIColor.white.cgColor
}
func loadViewFromNib() -> UIView? {
let bundle = Bundle(for: type(of: self))
let nib = UINib(nibName: String(describing: type(of: self)), bundle: bundle)
return nib.instantiate(withOwner: self, options: nil).first as? UIButton
}
#IBAction func pressed(_ sender: Any) {
print("Called in here")
}
}
I can then create a new button in my storyboard and set it to custom and the class to SampleButton. However now if I ctrl + drag from my button to my corresponding View Controller to create an IBAction for the button, it's not called. The one in the SampleButton.swift file is. Even if I delete the IBAction in the SampleButton file it's still not called.
Any help here? I want to be able to design the buttons separately and then have IBactions for them in the controllers where they're used.
I encountered this same issue with some of my custom xib views and my initial thought was that I could set up my xib to be IBDesignable and then connect outlets from the storyboard rendering of my button in the view controller.
That didn't work.
So I setup a bit of a workaround using delegate callbacks from my custom views. I created IBOutlets for the view to the view controllers using them, then in viewDidLoad I'd set the delegate and handle the button tap in the view controller
import UIKit
// defines a callback protocol for the SampleButtonView
protocol SampleButtonViewDelegate: class {
func sampleButtonTapped(_ button: SampleButton)
}
#IBDesignable
class SampleButton: UIView, NibLoadable {
// create IBOutlet to button if you want to register a target/action directly
#IBOutlet var button: UIButton!
// set delegate if you want to handle button taps via delegate
weak var delegate: SampleButtonViewDelegate?
// initializers to make it so this class renders in view controllers
// when using IBDesignable
convenience init() {
self.init(frame: .zero)
}
override init(frame: CGRect) {
super.init(frame: frame)
loadFromNib(owner: self)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
loadFromNib(owner: self)
}
#IBAction func buttonTapped(_ sender: Any) {
delegate?.sampleButtonTapped(_ button: self)
}
}
// here's a sample ViewController using this view and the delegate callback method
class ViewController: UIViewController {
#IBOutlet var sampleButtonView: SampleButton!
override func viewDidLoad() {
super.viewDidLoad()
sampleButtonView.delegate = self
}
}
extension ViewController: SampleButtonViewDelegate {
func sampleButtonTapped(_ button: SampleButton) {
// TODO: run logic for button tap here
}
}
For completeness I'll also add this NibLoadable protocol definition here.
// I used this for the #IBDesignable functionality to work and actually render
// my xib layouts in the storyboard view controller layouts using this class
import UIKit
/// Defines an interface for UIViews defined in .xib files.
public protocol NibLoadable {
// the name of the associated nib file
static var nibName: String { get }
// loads the view from the nib
func loadFromNib(owner: Any?)
}
public extension NibLoadable where Self: UIView {
/// Specifies the name of the associated .xib file.
/// Defaults to the name of the class implementing this protocol.
/// Provide an override in your custom class if your .xib file has a different name than it's associated class.
static var nibName: String {
return String(describing: Self.self)
}
/// Provides an instance of the UINib for the conforming class.
/// Uses the bundle for the conforming class and generates the UINib using the name of the .xib file specified in the nibName property.
static var nib: UINib {
let bundle = Bundle(for: Self.self)
return UINib(nibName: Self.nibName, bundle: bundle)
}
/// Tries to instantiate the UIView class from the .xib file associated with the UIView subclass conforming to this protocol using the owner specified in the function call.
/// The xib views frame is set to the size of the parent classes view and constraints are set to make the xib view the same size as the parent view. The loaded xib view is then added as a subview.
/// This should be called from the UIView's initializers "init(frame: CGRect)" for instantiation in code, and "init?(coder aDecoder: NSCoder)" for use in storyboards.
///
/// - Parameter owner: The file owner. Is usually an instance of the class associated with the .xib.
func loadFromNib(owner: Any? = nil) {
guard let view = Self.nib.instantiate(withOwner: owner, options: nil).first as? UIView else {
fatalError("Error loading \(Self.nibName) from nib")
}
view.frame = self.bounds
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
addSubview(view)
}
}
You could also simply register the functions you defined in your view controller as the target/action functions for the button in the custom view.
override func viewDidLoad() {
super.viewDidLoad()
mySampleButtonView.button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)
}
#objc func buttonTapped(_ sender: UIButton) {
// handle button tap action in view controller here...
}
create iboutlet of button in nib class.
add you nib view in your viewcontroller where its needed.
add target for the button outlet.
try following code:
override func viewDidLoad() {
super.viewDidLoad()
let myButton = Bundle.main.loadNibNamed("myButtonxibName", owner: self, options: nil)?[0] as? myButtonxibClassName
myButton.button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)
self.view.addsubview(myButton)
}
#objc func buttonTapped() {}
You don't need a Xib for what you're trying to do. Remove the loadViewFromNib() and the pressed(_ sender: Any) functions from your class above. Change your setup() method to customize your button. I see that you want to add a border to it. Do something like this:
func setup() {
self.layer.borderWidth = 2
self.layer.borderColor = UIColor.white.cgColor
// * Any other UI customization you want to do can be done here * //
}
In your storyboard, drag and drop a regular UIButton wherever you want to use it, set the class in the attributes inspector to SampleButton, connect your IBOutlet and IBActions as necessary, and it should be good to go.
I don't think it's possible to do this. Simpler way is to just set the target and action in view controllers. Something like:
class VC: UIViewController {
func viewDidLoad() {
sampleButton.addTarget(self, action: #selector(didClickOnSampleButton))
}
}

Deinit never called on custom UIView

Hello I have a UIViewController which has 4 custom UIView. Deinit function inside UIViewController is called when I change the rootViewController but doesn't called inside custom UIView. Therefore, It persists in memory after I change root controller. If I call myCustomView.removeFromSuperView() inside ViewControllers Deinit function, It works. However, I don't wanna write it everytime in a viewcontrollers so I want to write it inside custom UIView class but Deinit inside Custom UIView never get called.
How I declare custom view:
lazy var myCustomView = WelcomeScreenButtonView(text: getLabelText(key: SUPPORTEDSERVICES), imageName: "img")
let firstStackView = UIStackView()
override func viewDidLoad(){
//I put it inside a UIStackView
firstStackView.addArrangedSubview(myCustomView)
view.addSubView(firstStackView)
}
deinit{
//If I do it like this, It works but I don't wanna call it in here
myCustomView.removeFromSuperView()
}
My Custom UIView Class:
class WelcomeScreenButtonView: UIView {
lazy var label = UILabel()
private lazy var logoImage = UIImageView()
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
setupConstraints()
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
//This one never get called
print("WelcomeScreen Button Deinited")
removeFromSuperview()
}
}
EDIT: If I change my custom view to UIView() (all other codes are same). It is deallocating. Therefore, I guess I have something to do with custom UIView class.

Custom UIView for an UIViewController in Swift

I use code to create the view (with subviews) for UIViewController's this is how I do it:
override loadView()
class MYViewController: UIViewController {
var myView: MyView! { return self.view as MyView }
override func loadView() {
view = MyView()
}
}
and here is how I create my custom view:
class MyView: UIView {
// MARK: Initialization
override init (frame : CGRect) {
super.init(frame : frame)
addSubviews()
setupLayout()
}
convenience init () {
self.init(frame:CGRect.zero)
}
required init(coder aDecoder: NSCoder) {
fatalError("This class does not support NSCoding")
}
// MARK: Build View hierarchy
func addSubviews(){
// add subviews
}
func setupLayout(){
// Autolayout
}
// lazy load views
}
I do this for all my View Controllers and I am looking for more elegant way, because this process is repetitive, so is there any solution for make that generic for example, create a super abstract class, or create an extension for UIViewController and UIView, Protocols ? I am new for swift and I think that Swift can have a better elegant solution with it's modern patterns
If you are wanting to create many different controllers with custom view classes my recommended solution would be along these lines:
First implement a custom view subclass the way you want to be able to use it, here I have used the one you had in your question. You can then subclass this anywhere you need it and just override the relevant methods.
class CustomView: UIView {
// MARK: Initialization
override init(frame: CGRect) {
super.init(frame: frame)
addSubviews()
setupLayout()
}
required init() {
super.init(frame: .zero)
addSubviews()
setupLayout()
}
required init(coder aDecoder: NSCoder) {
fatalError("This class does not support NSCoding")
}
// MARK: Build View hierarchy
func addSubviews(){
// add subviews
}
func setupLayout(){
// Autolayout
}
}
Then create a generic custom view controller that allows specification of a class as a generic parameter so that you can easily create a controller with a custom view class.
class CustomViewController<T: CustomView>: UIViewController {
var customView: T! { return view as! T }
override func loadView() {
view = T()
}
init() {
super.init(nibName: nil, bundle: nil)
}
}
Then if you wanted to define a new custom view and create a controller that uses it you can simply:
class AnotherCustomView: CustomView { /* Override methods */ }
...
let controller = CustomViewController<AnotherCustomView>()
Boom!
If you wanted you could even typealias this new controller type to make it even more elegant:
class AnotherCustomView: CustomView { /* Override methods */ }
...
typealias AnotherCustomViewController = CustomViewController<AnotherCustomView>
let controller = AnotherCustomViewController()

iOS auto layout's modification in custom view doesn't work until viewDidAppear gets called

I've got view controller (using Storyboards if matters). Controller got custom view inside it let's call it AView. The view is laid out on storyboard as UIView object with custom class set. AView's contents are on separate XIB because I need this highly reusable. Here's how code looks like:
class VC: UIViewController {
#IBOutlet weak var aView: AView!
override func viewDidLoad() {
super.viewDidLoad()
aView.setup(false) //doesn't work
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
aView.setup(false) //doesn't work
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
aView.setup(false) //do work but glitches
}
}
class AView: UIView {
required init?(coder aDecoder: NSCoder) {
//init stuff: loading nib, adding view from it
}
#IBOutlet weak var someView: UIView! //this view has all constraints which are required and additional rightConstraint which is inactive, for future use
#IBOutlet var leftConstraint: NSLayoutConstraint!
#IBOutlet var rightConstraint: NSLayoutConstraint!
func setup(shouldBeOnLeft: Bool) {
leftConstraint.active = true
rightConstraint.active = false
self.layoutIfNeeded()
}
}
I need to setup this view before it appears, based on some parameters. I'm modifying only its internal content from inside. If I call aView.setup(shouldBeOnLeft:) in viewDidLoad or viewWillAppear constraints don't update or maybe do but I don't see changes. If I move it to viewDidAppear it works but obviously I see misplaced views for a while (state before setup).
The question is: how to get it work as intended and without view's manipulation form view controller and independent on how and where setup method is called unless it's inside or right after VC's viewDidLoad? Only thing that VC needs to know is to call setup with parameter.
You should call it inside viewDidLayoutSubviews(), at this point it have set the view parameters, and you can manipulate the view before is presented to the user (so, no glitches)
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIViewController_Class/#//apple_ref/occ/instm/UIViewController/viewDidLayoutSubviews
Discussion
Called to notify the view controller that its view has just laid out its subviews.
OK I figured that out - but I don't actually like it in 100%
class VC: UIViewController {
#IBOutlet weak var aView: AView!
override func viewDidLoad() {
super.viewDidLoad()
aView.setup(false) //now it does work
}
}
class AView: UIView {
required init?(coder aDecoder: NSCoder) {
//init stuff: loading nib, adding view from it
}
#IBOutlet weak var someView: UIView! //this view has all constraints which are required and additional rightConstraint which is inactive, for future use
#IBOutlet var leftConstraint: NSLayoutConstraint!
#IBOutlet var rightConstraint: NSLayoutConstraint!
func setup(shouldBeOnLeft: Bool) {
self.yesIfLeft = shouldBeOnLeft
}
private var yesIfLeft = false {
didSet{
self.updateLayout()
}
}
private func updateLayout() {
leftConstraint.active = self.yesIfLeft
rightConstraint.active = !self.yesIfLeft
self.layoutIfNeeded()
}
override func layoutSubviews() {
super.layoutSubviews()
self.updateLayout()
}
}
Whole point is to keep reference to settings that cause layout change and update them also in layoutSubviews(). Doing this way VC doesn't have to know about internals of AView (that's a big plus for me) but requires that AView holds it's configuration which is some extra work to do. I'm not marking this answer as accepted for now because maybe someone will have some better idea.

Outlets linked to controls in a Static TableView are not initialized

I am trying to setup a master details navigation.
I use storyboard, master is a dynamic table and details is a static table.
I have a nameLabel setup as an outlet in the controller but when i try to access it in viewDidLoad, its still set to nil.
Instead of using prepareForSegue, I have used didSelectRowAtIndexPath which pushes the detail view like this: (because i'm using the TableViewBindingHelper, see https://github.com/ColinEberhardt/ReactiveTwitterSearch/tree/master/ReactiveTwitterSearch/Util)
func showLessonView(lessonVM: LessonViewModel) {
let lessonViewController = LessonViewController(WithViewModel: lessonVM)
self.navigationController?.pushViewController(lessonViewController, animated: true)
}
LessonViewController:
import Foundation
import ReactiveCocoa
class LessonViewController: UITableViewController {
#IBOutlet var lessonNameLabel: UILabel!
private var viewModel: LessonViewModel
init(WithViewModel viewModel: LessonViewModel){
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
bindData()
}
func bindData() {
// null here!
if (lessonNameLabel != nil) {
lessonNameLabel.rac_text <~ viewModel.name
}
}
}
How can I fix this?
Other sample code i have seen performs the navigation in segue which ends up calling the init(coder aDecoder: NSCoder) constructor and all the outlets are already initialized.
Because you initialise the view controller with the WithViewModel initialiser, it knows nothing about the storyboard and so the outlets are not hooked up. To get the outlets hooked up as specified in the storyboard, you need either to use a segue, or to use the storyboard's instantiateViewControllerWithIdentifier(identifier:) method to create the view controller. Either way, you can't (easily) pass the ViewModel as an argument for the initialisation, so you will need to expose the viewModel var (remove private) and set it separately in your showLessonView method. To use instantiateViewControllerWithIdentifier(identifier:), give your Lesson View Controller an identifier (say "LessonViewController") in the storyboard. Then amend your showLessonView as follows:
func showLessonView(lessonVM: LessonViewModel) {
let lessonViewController = self.storyboard!.instantiateViewControllerWithIdentifier(identifier:"LessonViewController") as! LessonViewController
lessonViewController.viewModel = lessonVM
self.navigationController?.pushViewController(lessonViewController, animated: true)
}
When a view controller is instantiated from a storyboard, the init(coder:) initialiser is used, so either remove the override of that method, or amend it to call the super implementation:
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}

Resources