Custom Views and Libraries

In this example, we will be reimplementing the switchview as a separate library. The following code can be found at gomatcha.io/matcha/examples/customview.

mkdir -p $GOPATH/src/gomatcha.io/matcha/examples/customview

A typical Matcha library will have the following directory structure.

* <library-name>
    * android
        * <android-studio-project>
    * ios
        * <xcode-project>
    * proto
        * <protobuf-files>
    * <go-files>...

Protobuf

Our switch will has two properties, the on/off value and enabled/disabled state. And when the user activates the switch, we need to somehow communicate the new on/off value back to Go. Matcha handles this by serializing views and events into protobufs. So lets create a file at proto/customview.proto with the following contents. Further details about the protobuf spec can be found here.

syntax = "proto3";
package matcha.examples.customview;

option go_package = "proto";
option objc_class_prefix = "CustomViewProto";
option java_package = "io.gomatcha.customview.proto";
option java_outer_classname = "CustomViewProto";

message View {
    bool value = 1;
    bool enabled = 2;
}

message Event {
    bool value = 1;
}

Protobuf files need to be compiled into Go, Java and ObjC in order to be used. So create a proto/gen.go file containing the below.

package proto

//go:generate bash -c "( cd $GOPATH/src && protoc --go_out=. gomatcha.io/matcha/examples/customview/proto/*.proto )"
//go:generate bash -c "( cd $GOPATH/src && protoc --java_out=gomatcha.io/matcha/examples/customview/android/CustomViewLib/customview/src/main/java gomatcha.io/matcha/examples/customview/proto/*.proto )"
//go:generate bash -c "( cd $GOPATH/src && protoc --objc_out=gomatcha.io/matcha/examples/customview/ios/Protobuf gomatcha.io/matcha/examples/customview/proto/*.proto )"

Now we can call go generate ./... to generate the supporting protobuf code.

Go

Create a go file at customview.go.

package customview

import (
    "fmt"
    "runtime"

    "github.com/gogo/protobuf/proto"
    protoview "gomatcha.io/matcha/examples/customview/proto"
    "gomatcha.io/matcha/layout/constraint"
    "gomatcha.io/matcha/view"
)

type CustomView struct {
    view.Embed
    Enabled  bool
    Value    bool
    OnSubmit func(value bool)
}

// NewCustomView returns an initialized CustomView instance.
func NewCustomView() *CustomView {
    return &CustomView{
        Enabled: true,
    }
}

// Build implements view.View.
func (v *CustomView) Build(ctx view.Context) view.Model {
    l := &constraint.Layouter{}
    l.Solve(func(s *constraint.Solver) {
        if runtime.GOOS == "android" {
            s.Width(61)
            s.Height(40)
        } else {
            s.Width(51)
            s.Height(31)
        }
    })
    return view.Model{
        Layouter:       l,
        NativeViewName: "gomatcha.io/matcha/view/switch",
        NativeViewState: &protoview.View{
            Value:   v.Value,
            Enabled: v.Enabled,
        },
        NativeFuncs: map[string]interface{}{
            "OnChange": func(data []byte) {
                event := &protoview.Event{}
                err := proto.Unmarshal(data, event)
                if err != nil {
                    fmt.Println("error", err)
                    return
                }

                v.Value = event.Value
                if v.OnSubmit != nil {
                    v.OnSubmit(v.Value)
                }
            },
        },
    }
}

iOS

Writing a library for Matcha is similar to building an app. We start by creating a new Xcode project, containing a Cocoa Touch Framework.

ios-1

Drag the project into your app’s Workspace. Again, we need to make some changes to the Xcode project settings.

Add the Protobuf folder into your project and disable ARC by adding the -fno-objc-arc to any protobuf files in Build Phases > Compile Sources.

Custom views on iOS must conform to one of two protocols, MatchaChildView or MatchaChildViewController depending on whether you are wrapping a UIView or UIViewController. For this example we will implement a Switch by subclassing UIView.

Replace CustomView.h with…

#import <UIKit/UIKit.h>
#import <Matcha/Matcha.h>

@interface CustomView : UIView <MatchaChildView>
@property (nonatomic, strong) MatchaViewNode *viewNode;
@property (nonatomic, strong) MatchaBuildNode *node;
@property (nonatomic, strong) UISwitch *switchView;
@end

And add file CustomView.m with…

#import "CustomView.h"
#import "Customview.pbobjc.h"

@implementation CustomView

- (id)initWithViewNode:(MatchaViewNode *)viewNode {
    if ((self = [super initWithFrame:CGRectZero])) {
        self.viewNode = viewNode;
        [self addTarget:self action: @selector(onChange:) forControlEvents: UIControlEventValueChanged];
    }
    return self;
}

- (void)setNativeState:(NSData *)nativeState {
    CustomViewProtoView *view = [CustomViewProtoView parseFromData:nativeState error:nil];
    [self setOn:view.value animated:true];
    self.enabled = view.enabled;
}

- (void)onChange:(id)sender {
    CustomViewProtoEvent *event = [[CustomViewProtoEvent alloc] init];
    event.value = self.on;
    [self.viewNode call:@"OnChange", [[MatchaGoValue alloc] initWithData:event.data], nil];
}

@end

We must register the view class with the Matcha framework, so also add the following into your implementation file.

+ (void)load {
    [MatchaViewController registerView:@"github.com/overcyn/customview" block:^(MatchaViewNode *node){
        return [[CustomView alloc] initWithViewNode:node];
    }];
}

Android

Create a new Android Studio project called CustomViewLib in the android directory. Remove the default app module and add an Android Library module named customview.

android-1

Again open your project’s settings.gradle and include the Matcha project.

include ':matcha'
project(':matcha').projectDir = new File("${System.env.GOPATH}/src/gomatcha.io/matcha/android/MatchaLib/matcha")

And open your module’s build.gradle and add $GOPATH/gomatcha.io/matcha/android to the repositories and the matcha project to the dependencies.

repositories {
    flatDir {
        dirs "${System.env.GOPATH}/src/gomatcha.io/matcha/android"
    }
}

dependencies {
    compile project(':matcha')
}

Create a new class called CustomView with the following.

package io.gomatcha.customview;

import android.content.Context;
import android.support.v7.widget.SwitchCompat;
import android.util.DisplayMetrics;
import android.widget.CompoundButton;

import com.google.protobuf.InvalidProtocolBufferException;

import io.gomatcha.bridge.GoValue;
import io.gomatcha.customview.proto.CustomViewProto;
import io.gomatcha.matcha.MatchaChildView;
import io.gomatcha.matcha.MatchaView;
import io.gomatcha.matcha.MatchaViewNode;

public class CustomView extends MatchaChildView {
    MatchaViewNode viewNode;
    SwitchCompat view;
    boolean checked;

    static {
        MatchaView.registerView("github.com/overcyn/customview", new MatchaView.ViewFactory() {
            @Override
            public MatchaChildView createView(Context context, MatchaViewNode node) {
                return new CustomView(context, node);
            }
        });
    }

    public CustomView(Context context, MatchaViewNode node) {
        super(context);
        viewNode = node;

        float ratio = (float)context.getResources().getDisplayMetrics().densityDpi / DisplayMetrics.DENSITY_DEFAULT;
        view = new SwitchCompat(context);
        view.setPadding(0, 0, (int)(7*ratio), 0);
        view.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                if (isChecked != checked) {
                    checked = isChecked;
                    CustomViewProto.Event event = CustomViewProto.Event.newBuilder().setValue(isChecked).build();
                    CustomView.this.viewNode.call("OnChange", new GoValue(event.toByteArray()));
                }
            }
        });
        addView(view);
    }

    @Override
    public void setNativeState(byte[] nativeState) {
        super.setNativeState(nativeState);
        try {
            CustomViewProto.View proto = CustomViewProto.View.parseFrom(nativeState);
            checked = proto.getValue();
            view.setChecked(proto.getValue());
            view.setEnabled(proto.getEnabled());
        } catch (InvalidProtocolBufferException e) {
        }
    }
}