Custom UIView for an UIViewController in Swift - ios

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()

Related

UIStackView: Can I constrain the views which can be inserted into to conform to a protocol?

Instead of letting any UIView be inserted into a UIStackView, can I set it so that only views which conform to a custom protocol, say 'MyProtocol' can be inserted?
You could subclass uiview and make it accept the view and the protocol (from swift 4+, see https://stackoverflow.com/a/45276465/8517882)
It would look something like this:
protocol SomeProtocol {
func someFunc()
}
class CustomStack: UIView {
private let stack = UIStackView()
init() {
super.init(frame: CGRect.zero)
self.addSubview(stack)
// then you can constraint the stack to self
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func addSubview(_ view: UIView & SomeProtocol) {
self.stack.addSubview(view)
view.someFunc() // you can call the protocol methods on the view
}
func addArrangedSubviews(_ view: UIView & SomeProtocol) {
stack.addArrangedSubview(view)
view.someFunc() // you can call the protocol methods on the view
}
}

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.

Swift how to create a generic MVP like UIViewController

I want to remove repetitive code so I would like to create a simple MVP base view controller that will tie together a model, view and presenter types and automatically connect them e.g.:
class BaseMvpViewController<M: MvpModel, V: MvpView, P: MvpPresenter>: UIViewController {
Where my model and view are empty protocols:
protocol MvpModel {}
protocol MvpView: class {} // class is needed for weak property
and presenter looks like this:
protocol MvpPresenter {
associatedtype View: MvpView
weak var view: View? { get set }
func onAttach(view: View)
func onDetach(view: View)
}
This is my whole BaseMvpViewController:
class BaseMvpViewController<M: MvpModel, V, P: MvpPresenter>: UIViewController, MvpView {
typealias View = V
var model: M? = nil
var presenter: P!
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
deinit {
presenter.onDetach(view: self as! View)
}
override func viewDidLoad() {
createPresenter()
super.viewDidLoad()
presenter.onAttach(view: self as! View)
}
func createPresenter() {
guard presenter != nil else {
preconditionFailure("Presenter was not created or it was not assigned into the `presenter` property!")
}
}
}
The problem is that the V must be without the protocol i.e. cannot be V: MvpView. Otherwise specific implementation of a VC must have a class/struct and not just a protocol for the MvpView. All my views are just protocols and my VCs will implement them e.g.
class MyViewController: BaseMvpViewController<MyModel, MyView, MyPresenter>, MyView
Now the compiler complains in the onAttach() and onDetach() methods that "argument type 'V' does not conform to expected type 'MvpView'"
So I tried an extension:
extension BaseMvpViewController where V: MvpView {
override func viewDidLoad() {
presenter.onAttach(view: self as! View)
}
}
yet another compiler error: "cannot invoke 'onAttach' with an argument list of type '(view: V)'". There is another small compilation error "Members of constrained extensions cannot be declared #objc" where I override func viewDidLoad() in the extension. This can be fixed by my own method and calling that one from viewDidLoad in the custom class. Any idea how to achieve what I want?
This is a similar/same issue like Using some protocol as a concrete type conforming to another protocol is not supported but maybe something has been improved in the Swift world since then. Or did I really hit a hard limit in the current Swift's capabilities?
In have finally found a solution, the problem was in casting self as! View, it must be self as! P.View. And there cannot be a base protocol for view because protocols do not conform to themselves in Swift. Here is my complete code:
protocol MvpPresenter {
associatedtype View
var view: View? { get set }
var isAttached: Bool { get }
func onAttach(view: View)
func onDetach(view: View)
}
/// Default implementation for the `isAttached()` method just checks if the `view` is non nil.
extension MvpPresenter {
var isAttached: Bool { return view != nil }
}
class BaseMvpViewController<M, V, P: MvpPresenter>: UIViewController {
typealias View = V
var viewModel: M? = nil
private(set) var presenter: P!
//MARK: - Initializers
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override public init(nibName: String?, bundle: Bundle?) {
super.init(nibName: nibName, bundle: bundle)
}
deinit {
presenter.onDetach(view: self as! P.View)
}
//MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
presenter = createPresenter()
}
override func viewWillAppear(_ animated: Bool) {
guard let view = self as? P.View else {
preconditionFailure("MVP ViewController must implement the view protocol `\(View.self)`!")
}
super.viewWillAppear(animated)
if (!presenter.isAttached) {
presenter.onAttach(view: view)
}
}
//MARK: - MVP
/// Override and return a presenter in a subclass.
func createPresenter() -> P {
preconditionFailure("MVP method `createPresenter()` must be override in a subclass and do not call `super.createPresenter()`!")
}
}
And a sample VC:
class MyGenericViewController: BaseMvpViewController<MyModel, MyView, MyPresenter>, MyView {
...
override func createPresenter() -> MainPresenter {
return MyPresenter()
}
...
}
This VC will automatically have a viewModel property of type MyModel (could be anything e.g. struct, class, enum, etc), property presenter of type MyPresenter and this presenter will be automatically attached between viewDidLoad and viewWillAppear. One method must be overridden, the createPresenter() where you must create and return a presenter. This is called before the custom VC's viewDidLoad method. Presenter is detached in the deinit.
The last problem is that generic view controllers cannot be used in interface builder (IB), because IB talks to code via Objective-C runtime and that does not know true generics, thus does not see our generic VC. The app crashes when instantiating a generic VC from a storyboard/xib. There is a trick though that fixes this. Just load the generic VC manually into the Objective-C runtime before any instantiation from storyboard/xib. Good is in AppDelegate's init method:
init() {
...
MyGenericViewController.load()
...
}
EDIT 1:
I have found the loading of generic VC into Objective-C runtime in this SO answer https://stackoverflow.com/a/43896830/671580
EDIT 2:
Sample presenter class. The mandatory things is the typealias, the weak var view: View? and the onAttach & onDetach methods. Minimum implementation of the attach/detach methods is also provided.
class SamplePresenter: MvpPresenter {
// These two are needed!
typealias View = SampleView
weak var view: View?
private let object: SomeObject
private let dao: SomeDao
//MARK: - Initializers
/// Sample init method which accepts some parameters.
init(someObject id: String, someDao dao: SomeDao) {
guard let object = dao.getObject(id: id) else {
preconditionFailure("Object does not exist!")
}
self.object = object
self.dao = dao
}
//MARK: - MVP. Both the onAttach and onDetach must assign the self.view property!
func onAttach(view: View) {
self.view = view
}
func onDetach(view: View) {
self.view = nil
}
//MARK: - Public interface
/// Sample public method that can be called from the view (e.g. a ViewController)
/// that will load some data and tell the view to display them.
func loadData() {
guard let view = view else {
return
}
let items = dao.getItem(forObject: object)
view.showItems(items)
}
//MARK: - Private
}

Passing data between custom view and view controller

I've got a custom UIControl (TestControl) and want to pass a simple string to a label on the main view. The UIControl (TestControl) sits inside a UIView (CustomView) which has been placed on the storyboard using a view which has got the custom class (CustomView).
What would be a simple implementation that would take care of that?
I noticed that the CustomView is called before the viewDidLoad() in the ViewController.
ViewController.swift
import UIKit
#IBOutlet var someLabel: UILabel!
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
TestControl.swift
import UIKit
class TestControl: UIControl {
// Initializer
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.blackColor()
//this is where it would be good if a string could be passed to a label
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
CustomView.swift
import UIKit
#IBDesignable class CustomView: UIView{
#if TARGET_INTERFACE_BUILDER
override func willMoveToSuperview(newSuperview: UIView?) {
let testing: TestControl = TestControl(frame: self.bounds)
self.addSubview(testing)
}
#else
override func awakeFromNib() {
super.awakeFromNib()
let testing: TestControl = TestControl(frame: self.bounds)
self.addSubview(testing)
}
#endif
}
Since you have a control in a storyboard, all you should have to do is to hook up the the didChange event handler to an IBAction in your view controller. This can then interpret this action to do what you want.

Resources