Skip to content

Writing a New View

Introduction

This guide will use the Button class implementation as an example to demonstrate the steps needed to extend the Boden Framework with new view components.

In contrast to Writing a View Module, this guide will explain how to extend the Boden Framework itself with a new integrated view component rather than adding an optional component which can be shipped independently. Please refer to Writing a View Module if you want to create a standalone view component.

The View

Boden views consist of two parts:

  1. The outward facing platform-independent View class. This is what users of your view component will usually see and interact with.
  2. The internal platform-specific View::Core implementation. This part of the view implementation is usually hidden from users of your new view component.

The first step in writing a new view is to create a new class derived from View.

Create a new header in framework/ui/include/bdn called ExampleButtonView.h:

#pragma once
#include <bdn/ui/View.h>

namespace bdn
{
    class ExampleButtonView : public ui::View
    {
      public:
        ExampleButtonView(
            std::shared_ptr<ViewCoreFactory> viewCoreFactory = nullptr);
    };
}

Source

In boden/framework/ui/src, create a file called ExampleButtonView.cpp:

#include <bdn/ExampleButtonView.h>

namespace bdn 
{
    ExampleButtonView::ExampleButtonView(
        std::shared_ptr<ViewCoreFactory> viewCoreFactory)
     : View(viewCoreFactory)
    {
    } 
}

The View Core

Create a new Core interface as an inner class of ExampleButtonView:

#pragma once
namespace bdn
{
    class ExampleButtonView : public ui::View
    {
        // ...
    public:
        class Core
        {
        };
    };
}
Full
#pragma once
namespace bdn
{
    class ExampleButtonView : public ui::View
    {
      public:
        ExampleButtonView(
            std::shared_ptr<ViewCoreFactory> viewCoreFactory = nullptr);
      public:
        class Core
        {
        };
    };
}

The iOS Implementation

Header

The core's actual implementation is platform-specific. Create a new Objective-C++ header in framework/ui/platforms/ios/include/bdn/ios/ called ExampleButtonViewCore.hh:

#pragma once

#include <bdn/ExampleButtonView.h>
#include <bdn/ExampleButtonViewCore.h>
#import <bdn/ios/ViewCore.hh>

namespace bdn::ios
{
    class ExampleButtonViewCore : public ViewCore, 
                                  virtual public bdn::ExampleButtonView::Core
    {
      public:
        ExampleButtonViewCore(
            const std::shared_ptr<bdn::ViewCoreFactory> &viewCoreFactory);
    };
}

Source

Create a new Objective-C++ source file in framework/ui/platforms/ios/src called ExampleButtonViewCore.mm:

#import <bdn/ios/ExampleButtonViewCore.hh>

@interface ExampleUIButton : UIButton <UIViewWithFrameNotification>
@property(nonatomic, assign) std::weak_ptr<bdn::ios::ExampleButtonViewCore> core;
@end

@implementation ExampleUIButton
- (void)setViewCore:(std::weak_ptr<bdn::ios::ViewCore>)viewCore
{
    self.core = std::dynamic_pointer_cast<bdn::ios::ExampleButtonViewCore>(viewCore.lock());
}
- (void)setFrame:(CGRect)frame
{
    [super setFrame:frame];
    if(auto core = self.core.lock()) {
        core->frameChanged();
    }
}

namespace bdn::ios
{
    ExampleButtonViewCore::ExampleButtonViewCore(const std::shared_ptr<ViewCoreFactory>& viewCoreFactory)
    : ViewCore(viewCoreFactory, [ExampleUIButton buttonWithType:UIButtonTypeSystem])
    {

    }
}

Note

For more information about the intricacies of implementing view cores please refer to ViewCore and iOS ViewCore.

Connecting the View Core to the View

Boden uses the ViewCoreFactory to create instances of the view core. To create the connection between a view and its core, add the following boilerplate to the classes:

View Header

Add the following code to ExampleButtonView.h:

#include <bdn/ViewUtilities.h>

namespace bdn::detail
{
    VIEW_CORE_REGISTRY_DECLARATION(ExampleButtonView)
}
ExampleButtonView.h
#pragma once
#include <bdn/ui/View.h>
#include <bdn/ViewUtilities.h>

namespace bdn
{
    namespace detail
    {
        VIEW_CORE_REGISTRY_DECLARATION(ExampleButtonView)
    }
    class ExampleButtonView : public ui::View
    {
      public:
        ExampleButtonView(
            std::shared_ptr<ViewCoreFactory> viewCoreFactory = nullptr);
      public:
        class Core
        {
        };
    };
}

View Implementation

Add the following code to ExampleButtonView.cpp:

namespace bdn::detail
{
    VIEW_CORE_REGISTRY_IMPLEMENTATION(ExampleButtonView)
}

ExampleButtonView::ExampleButtonView(
    std::shared_ptr<ViewCoreFactory> viewCoreFactory)
 : View(viewCoreFactory)
{
    bdn::detail::VIEW_CORE_REGISTER(ExampleButtonView, View::viewCoreFactory());
}
ExampleButtonView.cpp
#include <bdn/ExampleButtonView.h>

namespace bdn 
{
    namespace detail
    {
        VIEW_CORE_REGISTRY_IMPLEMENTATION(ExampleButtonView)
    }

    ExampleButtonView::ExampleButtonView(
        std::shared_ptr<ViewCoreFactory> viewCoreFactory)
     : View(viewCoreFactory)
    {
        bdn::detail::VIEW_CORE_REGISTER(ExampleButtonView, View::viewCoreFactory());
    } 
}

View Core Implementation

Add the following code to ExampleButtonViewCore.mm:

namespace bdn::detail
{
    CORE_REGISTER(ExampleButtonView, 
                  bdn::ios::ExampleButtonViewCore, 
                  ExampleButtonView)
}
ExampleButtonViewCore.mm
#import <bdn/ios/ExampleButtonViewCore.hh>

@interface ExampleUIButton : UIButton <UIViewWithFrameNotification>
@property(nonatomic, assign) std::weak_ptr<bdn::ios::ExampleButtonViewCore> core;
@end

@implementation ExampleUIButton
- (void)setViewCore:(std::weak_ptr<bdn::ios::ViewCore>)viewCore
{
    self.core = std::dynamic_pointer_cast<bdn::ios::ExampleButtonViewCore>(viewCore.lock());
}
- (void)setFrame:(CGRect)frame
{
    [super setFrame:frame];
    if (auto core = self.core.lock()) {
        core->frameChanged();
    }
}
@end

namespace bdn::detail
{
    CORE_REGISTER(ExampleButtonView, bdn::ios::ExampleButtonViewCore, ExampleButtonView)
}

namespace bdn::ios
{
    ExampleButtonViewCore::ExampleButtonViewCore(const std::shared_ptr<bdn::ViewCoreFactory> &viewCoreFactory)
        : ViewCore(
            viewCoreFactory, 
            [ExampleUIButton buttonWithType:UIButtonTypeSystem])
    {
    }
    ExampleButtonViewCore::~ExampleButtonViewCore()
    {
    }
}

Adding Properties

To represent properties of a view, use the Property class.

As an example, add a label property so users of your view can set the button's label.

class ExampleButtonView : public ui::View
{
public:
    Property<std::string> label;
}
ExampleButtonView.h
#pragma once
#include <bdn/ui/View.h>

namespace bdn
{
    namespace detail
    {
        VIEW_CORE_REGISTRY_DECLARATION(ExampleButtonView)
    }
    class ExampleButtonView : public ui::View
    {
      public:
        Property<std::string> label;

      public:
        ExampleButtonView(
            std::shared_ptr<ViewCoreFactory> viewCoreFactory = nullptr);

      public:
        class Core
        {
        };
    };
}

To connect the property to the core, replicate the property in the Core interface:

class Core
{
public:
    Property<std::string> label; 
}
ExampleButtonView.h
#pragma once
#include <bdn/ui/View.h>

namespace bdn
{
    namespace detail
    {
        VIEW_CORE_REGISTRY_DECLARATION(ExampleButtonView)
    }
    class ExampleButtonView : public ui::View
    {
      public:
        Property<std::string> label;

      public:
        ExampleButtonView(
            std::shared_ptr<ViewCoreFactory> viewCoreFactory = nullptr);

      public:
        class Core
        {
          public:
            Property<std::string> label;
        };
    };
}

To bind the two label properties in View and Core properties, override the View::bindViewCore() function:

void ExampleButtonView::bindViewCore()
{
    View::bindViewCore();
    auto buttonCore = core<ExampleButtonView::Core>();
    buttonCore->label.bind(label);
}
ExampleButtonView.cpp
#include <bdn/ExampleButtonView.h>

namespace bdn 
{
    namespace detail
    {
        VIEW_CORE_REGISTRY_IMPLEMENTATION(ExampleButtonView)
    }

    ExampleButtonView::ExampleButtonView(
        std::shared_ptr<ViewCoreFactory> viewCoreFactory)
     : View(viewCoreFactory)
    {
        bdn::detail::VIEW_CORE_REGISTER(ExampleButtonView, View::viewCoreFactory());            
    }

    void ExampleButtonView::bindViewCore()
    {
        View::bindViewCore();
        auto buttonCore = core<ExampleButtonView::Core>();
        buttonCore->label.bind(label);
    }
}

Now it is easy to react to changes to label in the iOS Implementation by adding an onChange() handler in the view core's init function.

namespace bdn::ios {
    void ExampleButtonViewCore::init()
    {
        ViewCore::init();
        label.onChange() += [=](auto newValue) {
            auto uiButton = (UIButton*)uiView();
            [uiButton setTitle:bdn::fk::stringToNSString(value) forState:UIControlStateNormal]];
        };
    }
}
ExampleButtonViewCore.mm
#import <bdn/ios/ExampleButtonViewCore.hh>
#import <bdn/foundationkit/stringUtil.hh>

@interface ExampleUIButton : UIButton <UIViewWithFrameNotification>
@property(nonatomic, assign) std::weak_ptr<bdn::ios::ExampleButtonViewCore> core;
@end

@implementation ExampleUIButton
- (void)setViewCore:(std::weak_ptr<bdn::ios::ViewCore>)viewCore
{
    self.core = std::dynamic_pointer_cast<bdn::ios::ExampleButtonViewCore>(viewCore.lock());
}
- (void)setFrame:(CGRect)frame
{
    [super setFrame:frame];
    if (auto core = self.core.lock()) {
        core->frameChanged();
    }
}
@end

namespace bdn::detail
{
    CORE_REGISTER(ExampleButtonView, bdn::ios::ExampleButtonViewCore, ExampleButtonView)
}

namespace bdn::ios
{
    ExampleButtonViewCore::ExampleButtonViewCore(const std::shared_ptr<bdn::ViewCoreFactory> &viewCoreFactory)
        : ViewCore(
            viewCoreFactory, 
            [ExampleUIButton buttonWithType:UIButtonTypeSystem])
    {
    }
    ExampleButtonViewCore::~ExampleButtonViewCore()
    {
    }
    void ExampleButtonViewCore::init()
    {
        ViewCore::init();
        label.onChange() += [=](auto newValue) {
            auto uiButton = (UIButton*)uiView();
            [uiButton setTitle:bdn::fk::stringToNSString(value) forState:UIControlStateNormal]];
        };
    }
}

Sending Notifications to the View

To allow users of your new class to react to button clicks, you can use a combination of a Notifier and a WeakCallback.

First, add the WeakCallback to your Core:

class ExampleButtonView : public ui::View
{
public:
    class Core
    {
      public:
        WeakCallback<void()> _clickCallback;
    }
}
ExampleButtonView.h
#pragma once
#include <bdn/ui/View.h>

namespace bdn
{
    namespace detail
    {
        VIEW_CORE_REGISTRY_DECLARATION(ExampleButtonView)
    }
    class ExampleButtonView : public ui::View
    {
      public:
        Property<std::string> label;

      public:
        ExampleButtonView(
            std::shared_ptr<ViewCoreFactory> viewCoreFactory = nullptr);

      public:
        class Core
        {
          public:
            Property<std::string> label;

          public:
            WeakCallback<void()> _clickCallback;
        };
    };
}

Now you can fire the callback from your UIButton subclass:

- (void)clicked
{
    if (auto core = self.core.lock()) {
        core->_clickCallback.fire();
    }
}
ExampleButtonViewCore.mm
#import <bdn/ios/ExampleButtonViewCore.hh>
#import <bdn/foundationkit/stringUtil.hh>
@interface ExampleUIButton : UIButton <UIViewWithFrameNotification>
@property(nonatomic, assign) std::weak_ptr<bdn::ios::ExampleButtonViewCore> core;
@end

@implementation ExampleUIButton
- (void)setViewCore:(std::weak_ptr<bdn::ios::ViewCore>)viewCore
{
    self.core = std::dynamic_pointer_cast<bdn::ios::ExampleButtonViewCore>(viewCore.lock());
}
- (void)setFrame:(CGRect)frame
{
    [super setFrame:frame];
    if (auto core = self.core.lock()) {
        core->frameChanged();
    }
}
- (void)clicked
{
    if (auto core = self.core.lock()) {
        core->_clickCallback.fire();
    }
}
@end

namespace bdn::detail
{
    CORE_REGISTER(ExampleButtonView, bdn::ios::ExampleButtonViewCore, ExampleButtonView)
}

namespace bdn::ios
{
    ExampleButtonViewCore::ExampleButtonViewCore(const std::shared_ptr<bdn::ViewCoreFactory> &viewCoreFactory)
        : ViewCore(
            viewCoreFactory, 
            [ExampleUIButton buttonWithType:UIButtonTypeSystem])
    {
    }
    ExampleButtonViewCore::~ExampleButtonViewCore()
    {
    }
    void ExampleButtonViewCore::init()
    {
        ViewCore::init();
        label.onChange() += [=](auto newValue) {
            auto uiButton = (UIButton*)uiView();
            [uiButton setTitle:bdn::fk::stringToNSString(value) forState:UIControlStateNormal]];
        };
    }
}

Add the notifier and the callback receiver to your View class:

class ExampleButtonView
{
  public:
    Notifier<const ClickEvent &> _onClick;
    WeakCallback<void()>::Receiver _clickCallbackReceiver;
}
ExampleButtonView.h
#pragma once
#include <bdn/ui/View.h>

namespace bdn
{
    namespace detail
    {
        VIEW_CORE_REGISTRY_DECLARATION(ExampleButtonView)
    }
    class ExampleButtonView : public ui::View
    {
      public:
        Property<std::string> label;

      public:
        ExampleButtonView(
            std::shared_ptr<ViewCoreFactory> viewCoreFactory = nullptr);

      public:
        Notifier<const ClickEvent &> _onClick;
        WeakCallback<void()>::Receiver _clickCallbackReceiver;

      public:
        class Core
        {
          public:
            Property<std::string> label;

          public:
            WeakCallback<void()> _clickCallback;
        };
    };
}

Finally, connect it to the WeakCallback:

void ExampleButtonView::bindViewCore()
{
    _clickCallbackReceiver = buttonCore->_clickCallback.set([=](){
        ClickEvent evt(shared_from_this());
        _onClick.notify(evt);
    });
}
ExampleButtonView.cpp
#include <bdn/ExampleButtonView.h>

namespace bdn 
{
    namespace detail
    {
        VIEW_CORE_REGISTRY_IMPLEMENTATION(ExampleButtonView)
    }

    ExampleButtonView::ExampleButtonView(
        std::shared_ptr<ViewCoreFactory> viewCoreFactory)
     : View(viewCoreFactory)
    {
        bdn::detail::VIEW_CORE_REGISTER(ExampleButtonView, View::viewCoreFactory());            
    }

    void ExampleButtonView::bindViewCore()
    {
        View::bindViewCore();
        auto buttonCore = core<ExampleButtonView::Core>();
        buttonCore->label.bind(label);

        _clickCallbackReceiver = buttonCore->_clickCallback.set([=](){
            ClickEvent evt(shared_from_this());
            _onClick.notify(evt);
        });
    }
}

Congratulations, your first custom view should be working now!