カテゴリー
iOS RxSwift Swift

RxSwiftとUITableViewを連携し、複数のSectionのTableViewを作成

RxSwiftとUITableViewの設定は少し分かりづらかったので、ここにまとめてみます。

Sample Code on github

まず、”SectionModel”というものを作成します。これは、UITableViewをいくつかのセクションに分ける場合の、いくつかのセクションと、そのセクション内のTableViewCellの内容に対応しています。

// ConfigViewSectionModelはSectionModel (RxDatasourcesで定義されているstruct)のtypealiasとして作成します。

typealias ConfigViewSectionModel = SectionModel<ConfigViewSection, ConfigItem>

// ConfigViewSectionはenumとして作成し、それぞれのセクションに応じて作成します。この例では"parameter", "playType", "interval"という3つのセクションに対応しています。
enum ConfigViewSection {
	
	// number of sections as needed ...
	case parameter
	case playType
	case interval
	
}

// ConfigItemもenumとして作成
enum ConfigItem {
	
	// parameter section
	case gameMode
	case tempo
	case timeLimit
	case tries
	case counts
	case lowest
	case highest
	case pianoVolume
	case metronomevolume
	
	// type section
	case ascend
	case descend
	case quarterNote
	case eighthNote
	case dyad
	
	// interval section
	case minSecond
	case second
	case minThird
	case third
	case fourth
	case dimFifth
	case fifth
	case minSixth
	case sixth
	case minSeventh
	case seventh
	case octave
}

ViewControllerでは以下のように設定

class ConfigViewController: UIViewController {

  lazy var tableView: UITableView = {
    let tableView = UITableView()
    // カスタムTableViewCellはいくつでもRegister可能 (Identifierで管理)
    tableView.register(ConfigParameterTVCell.self, forCellReuseIdentifier: ConfigParameterTVCell.identifier)
    tableView.register(ConfigSwitchTVCell.self, forCellReuseIdentifier: ConfigSwitchTVCell.identifier)
    // セパレーターを消したい場合は.clearを指定
    tableView.separatorColor = UIColor.clear
    // AutoLayoutのためこのパラメータをfalseにする
    tableView.translatesAutoresizingMaskIntoConstraints = false
    return tableView
  }()

  lazy var datasource = RxTableViewSectionedReloadDataSource<ConfigViewSectionModel>(configureCell: configureCell)
	
  lazy var configureCell: RxTableViewSectionedReloadDataSource<ConfigViewSectionModel>.ConfigureCell = { [weak self] (dataSource, tableView, indexPath, _) in
    // indexPathによりdataSourceからitem (enum型のConfigItem)をゲットする
    let item = dataSource[indexPath]
    // itemをswitchする
    switch item {
      case .tempo:
        // まず、事前にregisterしてあるCustomTableViewCell (この場合はConfigParameterTVCell) をdequeueする
        let cell = tableView.dequeueReusableCell(withIdentifier: ConfigParameterTVCell.identifier, for: indexPath) as! ConfigParameterTVCell
        // その後、cellの各パラメータに対して処理を記載
        cell.label.text = "something something"
        return cell
      case .timeLimit:
        // 同様にcellをdequeueし処理する

        // 以下省略
  }
}

RxSwiftのSubject (この場合はBehaviorRelay)とのDataBindingは以下のようにします。

private func setupViewModel() {
  configItems.asObservable()
    .bind(to: tableView.rx.items(dataSource: datasource))
    .disposed(by: bag)
}

上記のconfigItemsは以下のように定義し、設定します。

// configItemsは[ConfigViewSectionModel]のBehaviorRelayで、初期値は空の配列
let configItems = BehaviorRelay<[ConfigViewSectionModel]>(value: [])

// 3つのセクションに応じてそれぞれConfigViewSectionModelを作成します。
func updateConfigItems() {
  let sections: [ConfigViewSectionModel] = [
    parameterSection(),
    playTypeSection(),
    intervalSection(),
  ]
  // configItemsに[ConfigViewSectionModel]を渡します
  configItems.accept(sections)
}

// parameterSectionの実装
func parameterSection() -> ConfigViewSectionModel {
  let items: [ConfigItem] = [
    .gameMode,
    .tempo,
    .timeLimit,
    .tries,
    .counts,
    .lowest,
    .highest,
    .pianoVolume,
    .metronomevolume,
  ]
  // このSectionModelのConstructorはRxDatasoucesで定義されている
  return ConfigViewSectionModel(model: .parameter, items: items)
}

// playTypeSectionの実装
func playTypeSection() -> ConfigViewSectionModel {
  let items: [ConfigItem] = [
    .ascend,
    .descend,
    .quarterNote,
    .eighthNote,
    .dyad,
  ]
  // このSectionModelのConstructorはRxDatasoucesで定義されている
  return ConfigViewSectionModel(model: .playType, items: items)
}

// intervalSectionの実装
func intervalSection() -> ConfigViewSectionModel {
  let items: [ConfigItem] = [
    .minSecond,
    .second,
    .minThird,
    .third,
    .fourth,
    .dimFifth,
    .fifth,
    .minSixth,
    .sixth,
    .minSeventh,
    .seventh,
    .octave,
  ]
  // このSectionModelのConstructorはRxDatasoucesで定義されている
  return ConfigViewSectionModel(model: .interval, items: items)
}
カテゴリー
iOS Swift

UITableViewを学ぶ

UITableViewはポイントを抑えさえすればとても簡単です。

class ViewController: UIViewController {
  // create an instance of UITableView in a lazy fashion
  lazy var tableView: UITableView = {
    let tableView = UITableView()
    return tableView
  }()
  // custom class for delegate and datasources
  let customClass = CustomClass.shared

  override func viewDidLoad() {
    super.viewDidLoad()
    // set a background color so the view is visible
    view.backgroundColor = .white
    // add the tableView to the view
    view.addSubview(tableView)
    // set the frame of the tableView to view.bounds
    tableView.frame = view.bounds
    tableView.register(UITableViewCell.self, forCellReuseIdentifier: "canBeAnythingUnique")
    // you can extend ViewController as the delegate and datasouces, or you can create a class for it.
    tableView.delegate = customClass
    tableView.dataSource = customClass
  }
}

// a custom class to handle datasources and delegate methods
class CustomClass: NSObject {
  // make this class singleton
  public static let shared = CustomClass()
  private override init() {
    super.init()
  }
}

// extend the CustomClass as UITableViewDatasource
extension CustomClass: UITableViewDataSource {
  // two required methods
  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    // in real world scenario, you would return the count of a data array
    return 10
  }
	
  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
    // a standard UITableViewCell has a textLabel as default
    cell.textLabel?.text = "Hello World!!"
    return cell
  }
}

extension CustomClass: UITableViewDelegate {
  // any delegation methods goes here...	
}

カスタムセルを作る場合は、UITableViewCellをエクステンドします。

class CustomTableViewCell: UITableViewCell {

  static let identifier = "CustomTableViewCell"
	
  override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
    super.init(style: style, reuseIdentifier: reuseIdentifier)
		
    contentView.backgroundColor = .orange
  }
	
  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }	
}
カテゴリー
iOS Swift

JSONをパースし、オブジェクトとして使用する

Web APIから入手したり、もしくはBundleに保存したJSONファイルをパースし、オブジェクトとして使用出来るようにする。

例えば以下のようなJSONがあったとして、、、

{
  "chords": [
    {
      "quality": "Major 7th",
      "voicing": "Drop 2 & 4",
      "inversion": "Root Position",
      "notes": "36,16,31,12",
      "root_note": 36
    },
    {
      "quality": "Major 7th",
      "voicing": "Drop 2 & 4",
      "inversion": "1st Inversion",
      "notes": "16,11,37,33",
      "root_note": 11
     }
  ]
}

この場合、以下のように対応するstructを作り

struct ServerResponse: Codable {
	let chords: [ResponseChord]
	
	struct ResponseChord: Codable, Hashable {
		let quality: String
		let voicing: String
		let inversion: String
		let notes: String
		let root_note: Int
	}
}

以下のように使用します。

guard let jsonPath = Bundle.main.path(forResource: "chords", ofType: "json") else { return }
do {
  let data = try Data(contentsOf: URL(fileURLWithPath: jsonPath))
  let jsonResult = try JSONDecoder().decode(ServerResponse.self, from: data)
  print(jsonResult)
  } catch {
			
}
カテゴリー
iOS Swift

UIViewの上下のみにボーダーをつける

class GuitarString: UIView {
	
	lazy var topBorder: UIView = {
		let line = UIView()
		line.translatesAutoresizingMaskIntoConstraints = false
		return line
	}()
	
	lazy var bottomBorder: UIView = {
		let line = UIView()
		line.translatesAutoresizingMaskIntoConstraints = false
		return line
	}()
	
	var lineWidth: CGFloat = 0.5
	var lineColor: UIColor = .black
	
	init(){
		super.init(frame: .zero)
		layout()
	}
	
	private func layout() {
		
		self.addSubview(topBorder)
		
		self.backgroundColor = .lightGray
		
		topBorder.leftAnchor.constraint(equalTo: self.leftAnchor).isActive = true
		topBorder.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
		topBorder.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true
		topBorder.heightAnchor.constraint(equalToConstant: lineWidth).isActive = true
		topBorder.backgroundColor = lineColor
		
		self.addSubview(bottomBorder)
		
		bottomBorder.leftAnchor.constraint(equalTo: self.leftAnchor).isActive = true
		bottomBorder.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
		bottomBorder.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true
		bottomBorder.heightAnchor.constraint(equalToConstant: lineWidth).isActive = true
		bottomBorder.backgroundColor = lineColor
		
	}
	
	required init?(coder: NSCoder) {
		fatalError("init(coder:) has not been implemented")
	}
}
カテゴリー
Laravel PHP

Laravel Migrationでデータベーステーブルにカラムを追加

カラムを追加する場合

// below command will generate a new migration file
php artisan make:migration add_omit_root_to_chords --table="chords"

// inside the migration file, write as below to add new columns

public function up()
{
        Schema::table('chords', function (Blueprint $table) {
            $table->boolean("omit_root")->nullable();
            $table->integer("genre")->nullable();
        });
    }
    //  ~~~~~~~~
}

カラムのタイプを変更する場合

// you need doctrine/dbal
composer require doctrine/dbal

// to create migration 
php artisan make:migration change_omit_root_type --table="chords"

// inside migration
Schema::table('chords', function (Blueprint $table) {
    $table->integer("omit_root")->change();
});
カテゴリー
Android Java RxJava

RxJavaを学ぶ(その3)

バックグラウンド・スレッドからメインスレッドにタスクを返したい場合は、subscribe()の前段にobserveOn(AndroidSchedulers.mainThread())を挿入する。これでバックグラウンド・タスクの結果によるUIのアップデートが可能。

下記はCompositeDisposableにdisposeを任せる例も兼ねて書いてみた。

Disposable isCountingDownDisposable = playMidi.isCountingDownSubject.observeOn(AndroidSchedulers.mainThread()).subscribe(value -> {
      if (value) {
        playButton.setImageResource(R.drawable.ic_round_loop_24);
        playButton.setPadding(10,10,10,10);
      } else {
        playButton.setImageResource(R.drawable.ic_play_fill);
        playButton.setPadding(-20,-20,-20,-20);
      }
    });
compositeDisposable.add(isCountingDownDisposable);
カテゴリー
Android Java

カスタムTouch Controlを作ってみた

スクリーン上で指を上下させることで値を増減させられるコントローラーを書いてみました。

private class OnAreaTouched implements View.OnTouchListener {

    double initialY = 0.0;
    double previousY = 0.0;

    @Override
    public boolean onTouch(View v, MotionEvent event) {
      switch(event.getAction()) {
        case MotionEvent.ACTION_DOWN:
          initialY = event.getY();
          break;
        case MotionEvent.ACTION_MOVE:
          double currentY = event.getY();
          // if finger initially went upwards, then changed it's direction to downwards
          if(currentY < initialY && currentY > previousY) {
            initialY = previousY;
          }
          // if finger initially went downwards, then changed it's direction to upwards
          if(currentY > initialY && currentY < previousY) {
            initialY = previousY;
          }
          double diff = (initialY - currentY) / 100;
          Log.d("MyTest", "initial: " + initialY + ", getY(): " + event.getY() + ", diff: " + diff);
          mValue = mValue + diff;
          // upper bound 400, lower bound 20
          mValue = Math.max(Math.min(mValue, 400), 20);
          String mValueString = "" + (int)mValue;
          SpannableString mValueSpanString = new SpannableString(mValueString);
          mValueSpanString.setSpan(new StyleSpan(Typeface.BOLD), 0, mValueSpanString.length(), 0);
          mValueTV.setText(mValueSpanString);
          previousY = event.getY();
          break;
        default:
          break;
      }
      return true;
    }
}
カテゴリー
Android Java

RelativeLayoutでViewをCenterに配置

public class MainActivity extends AppCompatActivity {

  BackgroundService bs;

  Button button1;
  Button button2;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    RelativeLayout rootLayout = new RelativeLayout(this);
    setContentView(rootLayout);

    bs = BackgroundService.getInstance();

    button1 = new Button(this);
    button1.setText("Task1");
    int button1Id = View.generateViewId();
    button1.setId(button1Id);
    RelativeLayout.LayoutParams buttonLP1 = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
    buttonLP1.addRule(RelativeLayout.CENTER_IN_PARENT);
    button1.setLayoutParams(buttonLP1);
    button1.setOnClickListener(new OnButtonClicked());

    rootLayout.addView(button1);

    button2 = new Button(this);
    button2.setText("Task2");
    RelativeLayout.LayoutParams buttonLP2 = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
    buttonLP2.addRule(RelativeLayout.BELOW, button1Id);
    buttonLP2.addRule(RelativeLayout.CENTER_HORIZONTAL);
    buttonLP2.topMargin = 20;
    button2.setLayoutParams(buttonLP2);

    rootLayout.addView(button2);
  }

  private class OnButtonClicked implements View.OnClickListener {

    @Override
    public void onClick(View v) {
      bs.backgroundTask1();
    }
  }

  private class OnButton2Clicked implements View.OnClickListener {

    @Override
    public void onClick(View view) {
      bs.backgroundTask2();
    }
  }
}
カテゴリー
Android Java

ClassをSingletonにするには

public class BackgroundService {
  
  public static BackgroundService getInstance() { return SingletonHolder.INSTANCE; }
  private static class SingletonHolder { private static final BackgroundService INSTANCE = new BackgroundService(); }
  private BackgroundService() {}

}
カテゴリー
Android Java RxJava

RxJavaを学ぶ(その2)

値を代入すると、Subscriberで何か処理がトリガーされるようなコード

Subject<Integer> intSubject = PublishSubject.create();
intSubject.subscribe(i -> Log.d(TAG, "Logging: " + i));


private class onButtonClicked implements View.OnClickListener {
    @Override
    public void onClick(View v) {
        intSubject.onNext(17);
    }
}

// when the button is clicked, Logs out "Logging: 17"