picon's Tech Blog

Talkroom / BATON を開発しているpicon inc.のテックブログです。

TalkroomのiOS高速開発を支える開発規約

こんにちは!piconの唯一のエンジニアの渋谷です!

piconでは、現在エンジニア一人の体制で開発を行っています。

事業拡大にむけて開発クオリティ・スピードともに全く足りていないので、 エンジニア(特にiOS, Android)の方を強く募集 してます!

 twitter.com

ただそんな体制の中でも、僕が その他の業務で忙殺されていない限り 以下のような比較的ハイペースで開発が実現出来ています。

  • Talkroom の初期バージョンは約1週間

  • 大型アップデートも1〜2週間程度

今回は、そんなpiconのiOS開発を支える開発規約について紹介したいと思います!

規約をきっちり決めている

前提としてpiconのiOS開発では、規約が比較的きっちり決まっています。

一人でもあえてがっちり決めてます。

規約が決まっていると、「どこにどんな風に書くか」など不要なところで思考をする必要がなくなるので、UIとかであればほぼ無心で開発できるようになりました。

 

読みやすさもグンと上がるのでとても気に入っています。(3ヶ月後の自分は他人であるのはなんども痛感しました...

 

ここでいう規約は、改行や記法の規約にとどまらず、レイヤーの分け方、ファイル内での関数の置き方、変数の定義の仕方など、比較的広い意味での規約です。

 

今回は、汎用性の高そうなUI周りの規約について紹介したいと思います。

※ PCでご覧になることをおすすめします...

 

UI周りの規約

picon ではストーリーボードを利用していません。ここは賛否両論あると思うので一旦触れずにおきます、、、w

 

UIViewControllerのクラスには、Viewの見た目とレイアウトに関することを記述します。

Viewだけにとどまらない処理については、ViewModelで処理するようにしています。

 

UIViewのサブクラスは以下の基準のいずれかを満たす場合にのみにしてます。

  • 処理もしくは構成が複雑である

  • 通化できるものである

  • 汎用性が高いものである

 

可読性を損ねるので、自作のUIViewサブクラスが自作のUIViewサブクラスを持たないようにしています。

 

UIViewControllerクラスのテンプレートは以下です。

 

import UIKit
import RxSwift

final class SampleViewController: UIViewController {

    // MARK: - Views -
    

    // 説明メモ:viewはlazyで定義して、プロパティの初期設定はここで行う
    // 各Viewのプロパティがかたまりとしてひと目で確認できるのが便利

    private lazy var sampleView: UIView = {
        let view = UIView()
        view.backgroundColor = UIColor.black
        view.clipsToBounds = true
        view.layer.cornerRadius = 10
        return view
    }()
    
    
    // MARK: - Properties -
    
    private let viewModel: SampleViewModelType
    private let disposeBag = DisposeBag()
    
    
    // MARK: - Initializer -
    
    // 説明メモ:viewModelは外部から注入する

    init(viewModel: SampleViewModelType) {
        super.init(nibName: nil, bundle: nil)
        
        self.viewModel = viewModel
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    
    // MARK - Life Cycle Events -
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        configure()
        setViews()
        setConstraints()
    }
    
    
    // MARK: - Setup -
    
    // 説明メモ:UIViewControllerに関する設定など

    private func configure() {
        view.backgroundColor = UIColor.white
    }
    
    // 説明メモ:viewへのaddはこちらで

    private func setViews() {
        view.addSubview(sampleView)
    }
    
    // 説明メモ:viewのレイアウトの制約はこちらで

    private func setConstraints() {
       sampleView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
    }
    
    // 説明メモ:viewのイベント処理はこちらで(piconではRxSwiftを利用してます)

    private func subscribeViews() {
        let tapGesture = UITapGestureRecognizer()
        view.addGestureRecognizer(tapGesture)
        
        tapGesture.rx.event
            .subscribe(onNext: { [weak self] () in
                // 説明メモ: 簡単な処理ではない限り、関数に切り出す
                self?.createUser()
            })
            .disposed(by: disposeBag)
    }
    
    // 説明メモ: viewModelのイベントはここでsubscribeする

    private func subscribeViewModel() {
    }
    

    // MARK: - Other -
    
    private func createUser() {
        // Do something.
    }
}

// 説明メモ:Protocolに準拠させる場合はextensionを利用して分離させます

extension SampleViewController: UITableViewDataSource {
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // ...
    }
}

 

ここまで決まっているとどこに何を書くのか迷いなく書けるようになります。

新たな技術を触る時に、ボイラープレート + コード規約が公開されてると便利だなって思ってます。

ぜひ参考にしてみてください!

おまけ

おまけにViewModelのテンプレートも掲載しておきます。こちらはオープンソース化されているkick starterのコードを参考にしています。

参考記事:

Kickstarter-iOSのViewModelの作り方がウマかった

inputとoutputを明示的にわかりやすくなるのでとても気に入ってます。

import RxSwift

protocol SampleViewModelInputs {
    let submitName: PublishSubject<String> { get }
}

protocol SampleViewModelOutputs {
    let submitedName: PublishSubject<Void> { get }
}

protocol SampleViewModelType {
    var inputs: SampleViewModelInputs { get }
    var outputs: SampleViewModelOutputs { get }
}

final class SampleViewModel: SampleViewModelType, SampleViewModelInputs, SampleViewModelOutputs {
    
    // MARK: - Properties
    
    var inputs: SampleViewModelInputs { return self }
    var outputs: SampleViewModelOutputs { return self }
    private let disposeBag = DisposeBag()

    
    // MARK: Inputs
    
    let submitName = PublishSubject<String>()
    
    
    // MARK: Outputs
    
    let submitedName = PublishSubject<Void>()
    
    
    // MARK: - Initializers
    
    init() {
        setBindings()
    }
    
    
    // MARK: - Binds -
    
    private func setBindings() {
        submitName
            .subscribe(onNext: { [weak self] (name) in
                guard let me = self else { return }
                
                self?.updateName(name: name)
                    .subscribe(onNext: { [weak self] _ in
                        // 説明メモ:処理
                        submitedName.on(.next(()))
                    })
                    .disposed(by: me.disposeBag)
            })
            .disposed(by: disposeBag)

    }
    
    
    // MARK: - Other -
    
    private func updateName(name: String) -> Observable<Void> {
        // ...
    }
}

 ---

piconでは、一緒にプロダクトを作っていく エンジニア(特にiOS, Android)を 全力で募集しています。

少しでも興味ある方は、ぜひ気軽にDMください!

まずは週末コミットやハッカソンのみの参加でも歓迎です。

↓ 連絡はこちら

twitter.com