4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

ライブラリ作ったから見てくれ

Posted at

申し訳ありません。「見て下さい。」ですね。
調子乗りました。

気を取り直して

こんにちはこんばんは。初投稿のいるべです。
昨年クリスマスに独り身でしにたくなったのでswiftの勉強を始めました。
情報系の学部にいながらプログラミングとは関わらない生活をしていたのでプログラミング歴は実質3ヶ月程となります。

今、自分のお気に入りのものを教えてよって主旨のアプリを作成してます。SNSは素人には無理ゲーとか言う声が聞こえてきそうですがまぁやるだけタダなので黙ってて下さい。

今回は表題の通り。そのアプリを作る上で欲しくなった機能をライブラリとして作成したので、「まぁ見てって下さいよ」というところでつらつらと書かせて下さい。
ただの宣伝にならないように色々と書けたらいいなと思います。

まず前提。なんでこんなものを作ったか、だ。

いやいや、その「こんなもの」を先に言えよと思う方もいらっしゃるかも知れんがスルーします。

UICollectionViewって意味分かんなくね?

玄人の皆様方に置かれましては**何が?**と思われるかも知れないが、

  • まずdelegateが意味わからん。
  • flowlayout何それ??
  • そもそもRunさせても何も表示されん、、
    その他壁は沢山あると言っても過言では無い。(断言)
    少なからず全く調べずにcollectionViewを使って意図した挙動を実装することができる人はそう多く無いのではと考えている(え?みんなふつーに書けるの、、?)

みんな悩んでるやろし作ったろ!

まぁもちろん既に上位互換ライブラリは存在していますが経験です。沢山勉強になりました。
僕と同じような初心者の皆様方も、「既にあるからそれ使おう」「既にあるから作るのやめよう」と言うのは悪くも無いけど良くも無いことがあるということを覚えておきましょう。たまには遠回りも必要ということです。

お待たせたしました。

今回作ったものはこちらになります。
CheckableTagImage.jpg
と画像をのっけてもインパクトがない。。
真ん中下のネコを妹に描いてもらった。ほんの1分程度でさらっと。すごい。

タグを簡単に生成したいやんってことでCheckableTagというライブラリを作成しました。

簡単に説明しますと、簡単にタグを作成できるライブラリです。
更に、タップしたら色が変わるようになっているのでチェックボックスのような使い方もできます。 
画像を見ての通り、タグの形にもいくつかあるので選ぶのに悩んじゃいますね?(☝︎ ՞ਊ ՞)☝︎

導入

現在はPodのみの対応です。

pod 'CheckableTag'
を追加してpod installして下さい。

使い方

import UIKit
import CheckableTag

class ViewController: UIViewController {
    
    let items = ["Hello"]

    let tagView: CheckableTag = {
        let view = CheckableTag()
        return view
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        tagView.frame = self.view.bounds
        self.view.addSubview(tagView)
        tagView.dataSource = self
        
    }
}

extension ViewController: CheckableTagDataSource {
    func getSelected(sender: CheckableTag) -> [Bool]? {
        return nil
    }
    func getItems(sender: CheckableTag) -> [String] {
        return items
    }
}

最低限のコードはこれだけです。
これで、下のような感じになります。

IMG_3691.jpg

*トリミングしてあります。
タップすると、
IMG_3692.jpg
色が変わります。

プロパティ

まぁここは興味があれば自由に弄ってみて下さい。
いるべ的には下記の設定が好きです。

//tagの形を変更
tagView.cellType = .curve
//tagの表示スタイルを変更
tagView.cellStyle = .groove
//ユーザが選択可能かどうか指定
tagView.canSelected = false
//フォントサイズの指定。fontsizeによってtagのサイズが変わる。
tagView.fontSize = 20

色の変更

/// 選択状態の色を指定する
/// - Parameters:
///   - text: テキストカラー
///   - back: バックグラウンドカラー
///   - line: 枠線カラー
tagView.setSelectedColor(text: .white, back: .red, line: .black)

/// 非選択状態の色を指定する
/// - Parameters:
///   - text: テキストカラー
///   - back: バックグラウンドカラー
///   - line: 枠線カラー
tagView.setUnSelectedColor(text: .gray, back: .white, line: .gray)

選択状態の取得

//存在する全てのタグの選択状態を取得します。
//trueのとき選択されている
//falseのとき非選択状態。
let isSelectedArray: [Bool] = tagView.getIsSelected()

細かいことを指定する場合

DataSourceの使用例

tagView.dataSource = self

------------------------------

extension ViewController: CheckableTagDataSource {
    ///初期選択状態を指定できる。
    ///canSelect = falseにしてこれと組み合わせればタグになる。
    func getSelected(sender: CheckableTag) -> [Bool]? {
        return [true, false, true, false, true, true]
    }
    
    ///tagを生成するためにタグのテキストを伝えられる。
    func getItems(sender: CheckableTag) -> [String] {
        return items
    }
}

Delegateの使用例

tagView.delegate = self

----------------------------

extension ViewController: CheckableTagDelegate {
    ///セルを選択したときの動作を設定できる。
    func didSelected(cell: CheckableCellProtocol) {
        print(cell.textLabel.text)
    }
}

Animationの使用例

tagView.animation = self

----------------------------

///このアニメーションは意味不明なのでただの参考として扱って下さい。
extension ViewController: TouchCellAnimationProtocol {
    ///タップし始めのときに行われるアニメーション。
    func touchStartAnimation(cell: CheckableCellProtocol) {
        UIView.transition(with: cell, duration: 1.0, options: [.transitionFlipFromTop], animations: nil, completion: nil)
    }
    ///タップ終わりのときに行われるアニメーション。
    func touchEndAnimation(cell: CheckableCellProtocol) {
        UIView.transition(with: cell, duration: 1.0, options: [.transitionFlipFromBottom], animations: nil, completion: nil)
    }
}

選択状態について

collectionViewやtableViewの選択状態を管理する時にスクロールしたら選択状態がずれていることがあります。
これはセルごとで選択状態を管理するのではなく、セル全ての選択状態を管理する配列を用意してそれを読んであげればそんな問題とはおさらばです。

///collectionviewを使っている上位view
    //cellに代入するテキストの配列。
    private(set) var items: [String] = []
    //cellの選択状態を管理する配列
    public lazy var isSelectedItems: [Bool] = {
        return [Bool].init(repeating: false, count: items.count)
    }()
///cellが生成されるタイミング
/// UICollectionViewDataSourceを継承

public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    //cellを再利用するために引っ張り出してくる
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)

    //選択状態を管理している配列から、今扱っているcellに該当する選択状態を見て、それに見合ったスタイルを反映する。
    if isSelectedItems[index.row] {
        cell.selectedColor(text: textColor, back: backColor, line: lineColor)
    } else {
        cell.unSelectedColor(text: textColor, back: backColor, line: lineColor)
    }
        
    return cell
}

Enumは便利。

Enumってすごいですね。僕は気持ちが先走ってよくタイポをしてしまう人間なのですが,
予測変換で出てこればタイポもないし、後からの要素の追加も楽々!今回はEnumと後述するProtocolに救われました。

public enum CellType: String {
    case square = "SquareCheckableCell"
    case curve = "CurveCheckableCell"
    case round = "RoundCheckableCell"
    case circle = "CircleCheckableCell"
}

これは実際のコードです。Stringがうだうだしてますね。
タイポしやすい文字列も
cellType.rawValue

を使えばここで設定している文字列が取得できますからタイポが一気に減ります。

最初はcase squareだけだったんですが、一度一通り作って仕舞えばもう簡単!
case *** と新しい要素をここに追加してビルドします(おい)
もちろんビルドは通りません。

そのビルドに対して出たエラーのところを修正すればあら不思議。
もうまた別の要素が使えるようになっているではないですか。

///指定されたcellのタイプを返す。
public func getCellType(collection: UICollectionView, index: IndexPath) -> CheckableCellProtocol {
        switch cellType {
        case .square:
            return collection.dequeueReusableCell(withReuseIdentifier: cellType.rawValue, for: index) as! SquareCheckableCell
        case .curve:
            return collection.dequeueReusableCell(withReuseIdentifier: cellType.rawValue, for: index) as! CurveCheckableCell
        case .round:
            return collection.dequeueReusableCell(withReuseIdentifier: cellType.rawValue, for: index) as! RoundCheckableCell
        case .circle:
            return collection.dequeueReusableCell(withReuseIdentifier: cellType.rawValue, for: index) as! CircleCheckableCell
        }
    }

今回はご覧の通り似てるけど違うものをいくつも作ったのでstrategyパターンを採用しました。(実は後付け)
これとenumのおかげで今後も変更がだいぶ楽になりそうです。

Protocolくんありがとう。

protocolの何が便利かというと後からの変更がとても楽です。追加も楽です。

protocolで決まりを作っておけば後はボーッとコーディングできる。
import Foundation
import UIKit

public protocol CheckableCellProtocol: UICollectionViewCell {
    ///cellの細かな表示を変更する
    var cellStyle: CellStyle { get set }
    ///cellとcontentViiewの間のマージン
    var margin: CGFloat { get }
    
    ///collectionviewに最初からあるview
    var contentView: UIView { get }
    ///textを表示するために一番中心に配置されるview
    var textLabel: UILabel { get }
    
    ///アニメーションを行うためのプロトコル
    var animationProtocol: TouchCellAnimationProtocol! { get set }
    ///labelに表示するテキストをセットする
    func setTextToTextLabel(textName: String)
    ///選択時の色設定
    func selectedColor(text textColor: CellColor?, back backColor: CellColor?, line lineColor: CellColor?)
    ///非選択時の色設定
    func unSelectedColor(text textColor: CellColor?, back backColor: CellColor?, line lineColor: CellColor?)
    
    ///cellを設定
    func setCell()
    ///normalstyleの設定。cellにぴったりくっつく
    func setNormalStyle()
    ///groovestyleの設定。cellとの間に少し溝を作る
    func setGrooveStyle()
}

///共通してるものはもう作っておく
extension CheckableCellProtocol {
    
    public var margin: CGFloat {
        return LayoutConstants.margin
    }
    
    public func setTextToTextLabel(textName: String) {
        textLabel.text = textName
    }
    
    public func selectedColor(text textColor: CellColor?, back backColor: CellColor?, line lineColor: CellColor?) {
        textLabel.textColor = textColor?.selectedColor ?? .white
        contentView.backgroundColor = backColor?.selectedColor ?? .init(red: 255 / 255, green: 100 / 255, blue: 100 / 255, alpha: 1)
        self.layer.borderColor = lineColor?.selectedColor?.cgColor ?? UIColor.gray.cgColor
    }
    
    public func unSelectedColor(text textColor: CellColor?, back backColor: CellColor?, line lineColor: CellColor?) {
        textLabel.textColor = textColor?.unSelectedColor ?? .gray
        contentView.backgroundColor = backColor?.unSelectedColor ?? .init(red: 230 / 255, green: 230 / 255, blue: 230 / 255, alpha: 1)
        self.layer.borderColor = lineColor?.unSelectedColor?.cgColor ?? UIColor.gray.cgColor
    }
    
    public func setCell() {
        switch cellStyle {
        case .normal:
            setNormalStyle()
        case .groove:
            setGrooveStyle()
        }
    }
    public func setNormalStyle() {
        textLabel.frame = contentView.bounds
    }
    public func setGrooveStyle() {
        contentView.frame = CGRect(x: self.bounds.minX + margin, y: self.bounds.minY + margin, width: self.bounds.width - margin * 2, height: self.bounds.height - margin * 2)
        textLabel.frame = contentView.bounds
    }
    
}

protocolのextensionを使えば、必ず同じな部分の実装を楽に共通化できるのでとても便利。まぁclassを継承しても良いんだけどprotocolは直接インスタンス化できないところが好き。

ストラテジーパターン

ストラテジーパターンを使うと同じようなものをいくつも作成する時に追加が楽。変更も楽。
やり方は、ルール(プロトコル)を作ってそのルールに則って種類の分かれる奴らをそれぞれ作る。今回で言えばCheckableCellProtocolを作ってそれに則ったcellをSquare, Curve, Round, Circleと作った。それらを利用する側は外部からそのプロトコルに則ったインスタンスを貰うかそれを特定できる情報をもらうようにすれば完成。後は簡単に追加削除変更なんでもござれ。
具体的には上記のgetCellType関数が肝になっているのでgithubから見てみてね。

さてこのライブラリの完成度は如何程か?

良かった点

  • 割と使いやすいのでは?
  • 変更しやすかった。楽しくコーディングできた。
  • 自分の理想にかなり近い。

改善点

  • はいまずUITestが通りません。意味がわかりません。調べたところdelegateが動作していないのが原因っぽいのですがだからと言ってどうすれば解決するのかさっぱりわからない。要勉強ですね。
  • delegateでタグ同士の間のスペースとか指定できるようにしたい。
  • github全部ニホンゴ。
  • コードレイアウトマンなのでstoryboardで使えるか確認していない。。(近日中に必ず確認して使えるようにするつもりではあるが)
  • CellTypeもCellStyleももっと種類増やしたい。
  • タグ左寄せの設定もできるようにしたい。

自己評価は120点!良くがんばりました。これからも頑張りましょう!(文句言うな。)

初心者をはじめこの記事を読んで下さった皆様方。

軽い感じで読みやすくしようと思って書いていましたがそうでもなくなってしまいましたね。
もっと色々書きたかったけど難しいし体力切れや。
飛ばし飛ばしでもここまで読んで下さってありがとうございました。解説の部分はあんまり参考にならなかったと思います。定期的にアウトプットしてしっかり言語化することが大事だと感じました。
改善点はまだまだありますがぜひ一度使ってみて下さい。そしてissueやPR投げてくださると嬉しいです。

なぜ「初心者をはじめ」と書いたかと言いますと、このコードは簡単なコードで書かれています。まぁ初心者が書いている+他人のコードなので読みにくいかも知れませんが。もっとプログラミング出来るようになりたいと考えている方は、OSSにPR投げたりしてcontributionを重ねていくことって大事だと思うのでそれのとっかかりとして、まずこのライブラリにPRを投げるのも良いと思うよってことが言いたかったです。
READMEのちょっとした修正やタイポ修正などでもこちらとしては本当に助かりますしいわゆるwin-winですね。(古い?)こんなのどうや?って気軽に投げて下さい。

※この記事の内容は変なこと言っている可能性も大いにあります。情報の取捨選択をうまく行って、僕には文句ではなく指摘をしてくださると踊って喜びます。

長文駄文失礼しました。ありがとうございました。

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?