Swift

【swift3入門】Xibファイルのレイアウトを設定しよう

はじめに

前回Modelを作成しようでモデルを作成しapiから取得したデータからある程度の項目を抽出して使用できるようにしました。
今回は実際に取得した記事情報をXibファイルを使って表示できるようにしましょう。

前回作成したモデル

前回ではこのようにモデルを作成しました。

struct Article {
      let title: String
      let body: String
      let renderedBody: String
      let urlString: String
      let userId: String
      let profileImageUrlString: String
 
      init(json: [String: Any]) {
          title = json["title"] as? String ?? ""
          body = json["body"] as? String ?? ""
          renderedBody = json["rendered_body"] as? String ?? ""
          urlString = json["url"] as? String ?? ""
          if let user = json["user"] as? [String: Any] {
              userId = user["id"] as? String ?? ""
              profileImageUrlString = user["profile_image_url"] as? String ?? ""
          } else {
              userId = ""
              profileImageUrlString = ""
          }
      }
 
      init() {
          title = ""
          body = ""
          renderedBody = ""
          urlString = ""
          userId = ""
          profileImageUrlString = ""
      }
  }

今回はこの中から、title(記事のタイトル),userId(ユーザー名),profileImageUrlString(サムネイルURL)を使用して記事セルを作成していこうと思います。

現状のレイアウト

TableViewを作成しようで作成したXibファイルはタイトルをとりあえず中心に表示させただけのテキトーなレイアウトです。ここから修正を加えていきましょう。

現状このようになっています。

不要なオートレイアウトの設定を解除

まず前回設定したオートレイアウトの設定を削除していきます。

現状の設定はこのように設定を確認したいパーツを選択した状態で、右上の定規みたいなマークのところを選択すると確認することができます。

このEditの部分を押せば細かい数値の編集をすることができます。

削除する場合は、例えば先程のスクショでの「Align Center X」を消したい場合は青色の枠になっている状態でこれは選択されている状態なのでこの状態でDeleteボタンを押せば消えてくれます。

このようになっていれば完了です。

IBOutletのコネクションの削除

現在TableViewCell.xibのLabelと、TableViewCell.swiftの

@IBOutlet weak var label: UILabel!

の部分がコネクションでつながっています。

今回UILabelを複数扱うのでこのようにlabelという命名だとわかりずらくなってしまうので違う名前に変更したいのですが、このようにコネクションでつながっているので単純に名前をコードで書き換えてしまうだけでは実行時エラーになってしまいます。それを避けるためにコネクションを削除する方法を知っておきましょう。

先程のようにコネクションを切りたいパーツを選択した状態で、右上の右矢印のマークを押すとコネクション一覧の画面が出てきます。「Referencing Outlets」の部分にlabelがTableViewCellとつながってるよっていうのが出てきています。この部分の☓ボタンを押せば消すことができます。

これで削除できました。先程13行目の左の部分が白丸になってたと思いますが、コネクションを削除した場合に中の白い部分が消えたかと思います。これはコネクションがあるかどうかを表していて白丸のときはつながっている状態を表しています。ただし、このようにXibファイルとswiftファイルを横並びにしたときの画面じゃないとつながっていても黒丸になる場合もあるので注意です。

コネクションが切れたので、コードの@IBOutlet weak var label: UILabel!の部分もあとで付け直すので一旦削除しておきましょう。

パーツを配置

今回使用するのは、

  • UILabelを使用して記事タイトルを表示
  • UILabelを使用してユーザー名を表示
  • UIImageViewを使用してユーザーのサムネ画像を表示

のようにしていきます。

イメージとしてはこのような配置にしようと思います。
この状態はまだオートレイアウトは設定していなくてただパーツをのせただけの状態です。

まずタイトル用のLabelの設定をしていきます。

上と左と右の余白を20pxに設定しました。
赤い点線のマークが実線になった部分が適用されます。ひとつコツがあって、tabボタンを押すと次のフォームに移動できるので使えると速く設定できます。
あと、「Constrain to margin」のチェックが入っていると自動でマージンが入ってしまうのでチェックを外したほうがやりやすいので外すようにしました。

次にユーザー名用のLabelの設定をしていきます。

上をタイトル用のLabelから20px、右と下の余白を20px、高さを20pxに設定しました。
タイトルの方では高さを定義しませんでしたが、こちらでは高さを定義しています。高さを定義しない場合セルのサイズに合わせて大きさが可変になってくれますが、可変の部分が複数ある場合、他の設定から大きさが1通りに決まらない場合はエラーになってしまいます。タイトルは大きさがばらばらでそれに応じた高さにしたいので可変にして、一方ユーザー名のほうはそこまで長くなくて1行で収まりそうなので高さ固定にしました。

右と下の余白はセルの外枠に対して20pxあけるようにしていますが、上はタイトル用のLabelから20pxあけるようにしました。このようにどのパーツに対して余白をあけるかどうかは
このように選択することができます。Content Viewがこの場合で言うとセルの外枠を表します。

サムネ画像用のImageViewの設定もします。

上をタイトル用のLabelから20px、右はサムネ用のLabelから20px、下はセルから20px、縦横比を固定に設定しました。
縦横比を設定するのは「Aspect ratio」にチェックを入れれば大丈夫ですが、現状の縦横比が自動で設定される形になるのでこの後編集する必要があります。
このように、テキトーにのせただけの状態のものが25:22になっていたので自動ではいってしまっていたので1:1に編集しました。

加えて、オートレイアウトの設定がここでは並んでいますが、どのパーツに対してどれぐらいあけるかというのは、この部分のSuperViewやLabelのように書かれていてここで確認することができます。SuperViewは親のViewという意味なのでここではCellを表します。Superは親という意味で使われることが多いので覚えておきましょう。

これでオートレイアウトの設定ができました。

青色で位置関係が表されているのでこれはちゃんと設定ができていることを表します。赤色がでてる場合はエラーです。どのような場合にエラーが出るかというと、セルの大きさがどのような場合でも1通りに中身のレイアウトが決まらない場合はエラーになります。

よくあるのが全て高さを固定してしまって特定の大きさのセルでしかレイアウトがうまくいかないみたいなパターンでのエラーなので注意が必要です。

IBOutletの設定

パーツの配置が終わったので次はデータを当てはめるための準備としてIBOutletの設定をしていきます。詳しいやり方はTableViewを作成しように書いてあるのでわからない人はこちらを読んで下さい。

このように設定しました

import UIKit

  class TableViewCell: UITableViewCell {

      @IBOutlet weak var titleLabel: UILabel!
      @IBOutlet weak var userIdLabel: UILabel!
      @IBOutlet weak var userThumbImageView: UIImageView!

      override func awakeFromNib() {
          super.awakeFromNib()
          // Initialization code
      }

      override func setSelected(_ selected: Bool, animated: Bool) {
          super.setSelected(selected, animated: animated)
          // Configure the view for the selected state
      }

      func bindData(article: Article) {
      }
  }

ポイントとしては命名を区別しやすいようにすることです。
僕は命名の際に、UILabelの場合はLabel、UIImageViewの場合はImageViewというように語尾につけるようにしています。
意識してないうちだとタイトル用のLabelに対してtitleと命名しちゃいがちだと思いますが、そうした場合に、このようにタイトルが渡ってきてそれをLabelにセットする関数を作成したときにタイトルがかぶってややこしくなってしまいます。

func setTitle(title: String) {
      title.text = title
  }

それを避けるために語尾にLabelやImageViewのようにつけるようにしています。

BindDataの実装

それではbindDataに各パーツに値を入れていく実装をしていきます。

このように書き換えました。

import UIKit

  class TableViewCell: UITableViewCell {

      @IBOutlet weak var titleLabel: UILabel!
      @IBOutlet weak var userIdLabel: UILabel!
      @IBOutlet weak var userThumbImageView: UIImageView!

      func bindData(article: Article) {
          titleLabel.text = article.title
          userIdLabel.text = article.userId
          setUserThumbImageView(imageUrlString: article.profileImageUrlString)
      }

      private func setUserThumbImageView(imageUrlString: String) {
          guard let profileImageUrl = URL(string: imageUrlString) else { return }
          let session = URLSession(configuration: .default)
          let downloadImageTask = session.dataTask(with: profileImageUrl) { (data, response, error) in
              guard let imageData = data else { return }
              let imageimage = UIImage(data: imageData)
              DispatchQueue.main.async() { () -> Void in
                  self.userThumbImageView.image = imageimage
              }
          }
          downloadImageTask.resume()
      }
  }

setUserThumbImageViewではURLから画像を取得してImageViewにセットしていますが、これはapiを叩いてTableViewに表示させるで使ったものとわりと似ていて同じ要領でできます。
このように1つのパーツに対して値を当てはめるコードが複数行に渡る場合はメソッドに切り分けて、bindDataではそのメソッドを呼ぶようにしましょう。

あとawakeFromNibとsetSelectedに関してはデフォルトで記述されていましたが不要なので削除しました。

これでXibファイルの設定は完了です!

レイアウト確認

実際にビルドしてみて確認してみたいところですが、ViewControllerの方でセルの高さを50として設定していて、おそらく小さすぎるのでとりあえず100で設定を変えてみてビルドしました。

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
      return 100
  }

このように設定しました。

ビルドした結果このようになりました。

いいかんじにできたと思いきや、よく見るとところどころタイトルが・・・で切れています。UILabelのデフォルトの設定ではLabelの大きさに対してはみ出てしまう量の文字列が来たときには最後を・・・となるようになっています。
タイトルのLabelの高さは、セルの高さが決まってからオートレイアウトで計算されます。
今回で言うと、

(titleLabelの高さ) = (セルの高さ) - (titleLabelの上の余白) - (titleLabelの下の余白) - (userIdLabelの高さ) - (userIdLabelの下の余白)

なので

100 - 20 - 20 - 20 - 20 = 20

となります。

titleのフォントサイズはデフォルトのままで17pxなので1行分しか高さがありませんでした。

なのでセルの高さをとりあえず130にしてみます。

もう一度ビルドしてみます。

このようにまだ直ってません。
しかし先程と比べて見るとタイトルの上下の余白が広くなったかなという印象です。

実際のLabelのサイズがどうなっているかはこのように確認することができます。再生ボタンみたいなマークのボタンを押すとこのモードを解除できます。
これからわかるようにLabelのサイズは2行分ぐらい入りそうな大きさになっていそうな雰囲気ですが1行しか表示してくれません。

これはtitleLabelの行数が1で設定されているのが原因です。UILabelではデフォルトの設定で行数が1になっています。なのでこれを変更することで解決します。

この部分で行数で変更することができます。
コードで指定することもできて、


titleLabel.numberOfLines = 1

のように設定することができます。
もし両方設定した場合はコードのほうが優先されます。

ここで行数を2として設定してもよいのですが、0を設定することができて、0の場合はLabelに渡された文字列の長さに応じて行数が可変になってくれます。なのでここでは0を設定しました。

もう一度ビルドします。

これで無事タイトルがちゃんと表示されました。

おわりに

今回でXibファイルのレイアウトを設定することができました。
簡単なレイアウトの表示でしたが、今回のような基本が出来るようになれば、パーツが増えたとしても基本の組み合わせでだいたい出来るので自分でいろいろいじってみてほしいです。

終盤にかけて、タイトルが2行になっても表示されるようにレイアウトを調整しましたが、まだ若干問題点が残っています。

  • もしタイトルが3行以上になる場合にまた語尾が・・・になってしまう。
  • タイトルが1行のときに余白が大きい気がする。

この2つの問題点が残っているのではないかなと思います。
なので次回はこれらの問題点を解決できるようにタイトルの長さに応じてセルの高さが変わるようにするというのを行いたいと思います。

今回のコードはこちらでも確認できるのでよかったらご確認ください。