文系プログラマの勉強ノート

スマホアプリ開発やデザインなどについて勉強したことをまとめています

【Xcode】UILabelの文字サイズを自動調整する

画面サイズに合わせてUILabelが拡大縮小する場合に、文字サイズも自動調整する方法。

何もしないとどうなるか

サンプルとして、5.5inch(ex. iPhone8 Plus)の画面サイズに合わせてレイアウトを作成しました。
青い四角部分がUILabelで、画面サイズに合わせてサイズが変わります。

まず iPhone8 Plusで表示した場合。問題ありません。
f:id:an3714106:20171129235157p:plain

次にiPhone4sで表示した場合。文字が省略されてしまいました。
これを調整していきます。
f:id:an3714106:20171129235212p:plain

1. Autoshrinkを有効にする

UILabelにはAutoshrinkという、UILabelのサイズに合わせて文字サイズを自動調整してくれるそのものずばりな機能があります。

Storyboardから設定する場合。
「Autoshrink」を「Minimum Font Scale」にし、下の欄でScaleを指定します。
Scaleは、自動調整時の文字サイズ最小値を、元々の文字サイズに対する比率で入力します。
f:id:an3714106:20171129235540p:plain

コードから設定する場合。

label.adjustsFontSizeToFitWidth = true
label.minimumScaleFactor = 0.3

f:id:an3714106:20171130000401p:plain

文字サイズが自動調整されました。
ただ、これだとラベルぎりぎりすぎるので、余白をつけたい場合は次に進みます。

2. drawTextで余白(padding)を付ける

UILabelのカスタムクラスを作成し、下記のようにdrawTextを実装します。

class AutoShrinkLabel: UILabel {
    
    var padding: UIEdgeInsets = UIEdgeInsetsMake(4, 4, 4, 4)

    override func drawText(in rect: CGRect) {
        let newRect = UIEdgeInsetsInsetRect(rect, padding)
        super.drawText(in: newRect)
    }
}

Storyboardからラベルに設定します。
f:id:an3714106:20171203230006p:plain

余白がつきました。
f:id:an3714106:20171203231831p:plain

大体これでいけるのですが、この方法は縦方向に余白を大きく取りたい場合に上手くいかないことがあります。
おそらく「label.adjustsFontSizeToFitWidth」というプロパティ名の通り、幅に合わせて自動調整しているため。

3. viewDidLayoutSubviewsで余白(padding)を付ける

drawTextでは調整しきれない場合は、UIViewControllerでviewDidLayoutSubviewsを実装して自力で計算します。
こうなるとdrawTextはなくてもいいかも。

class AutoShrinkLabel: UILabel {   
    var padding: UIEdgeInsets = UIEdgeInsetsMake(20, 4, 20, 4)
}

class ViewController: UIViewController {

    @IBOutlet weak var label: AutoShrinkLabel!
    
    override func viewDidLayoutSubviews() {
        let fontSize = label.frame.size.height - (label.padding.top + label.padding.bottom)
        label.font = label.font.withSize(fontSize)
    }
}

f:id:an3714106:20171203234559p:plain

以上です!

【Xcode】iOS11からUITableViewのSwipe Actionが新しくなった

f:id:an3714106:20171023224043p:plain:w250
iOS11からUITableViewDelegateにSwipe Actionの新しいメソッドが追加されました。
これによって以下のことができるようになりました。
(iOS11以降限定です)

  • 左から右へのSwipe Actionを実装する
  • Swipe Actionに画像を表示する



使用イメージ

f:id:an3714106:20171023224331p:plain:w250
左から右へのSwipe Action使用例です。
こんな感じで文字の代わりにアイコンを表示できるようになりました!



f:id:an3714106:20171023224318p:plain:w250
こちらは従来通り右から左へのSwipe Action、文字を表示した場合です。
こちらもアイコンに変えることができます。



サンプルコード

上記の使用イメージで使用したソースコードです。
storyboardで適当なUITableViewControllerと紐づけてください。

class ViewController: UITableViewController {
    
    let dataSource = ["One", "Two", "Three"];
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return dataSource.count
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = UITableViewCell(style: .default, reuseIdentifier: "Cell")
        cell.textLabel?.text = dataSource[indexPath.row]
        return cell
    }
    
    // iOS11以降
    // 右から左へスワイプ
    @available(iOS 11.0, *)
    override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
        
        let editAction = UIContextualAction(style: .normal,
                                            title: "Edit",
                                            handler: {(action: UIContextualAction, view: UIView, completion: (Bool) -> Void) in
            print("Edit")
            // 処理を実行完了した場合はtrue
            completion(true)
        })
        editAction.backgroundColor = UIColor(red: 101/255.0, green: 198/255.0, blue: 187/255.0, alpha: 1)
        
        let deleteAction = UIContextualAction(style: .destructive,
                                              title: "Delete",
                                              handler: { (action: UIContextualAction, view: UIView, completion: (Bool) -> Void) in
            print("Delete")
            // 処理を実行できなかった場合はfalse
            completion(false)
        })
        deleteAction.backgroundColor = UIColor(red: 214/255.0, green: 69/255.0, blue: 65/255.0, alpha: 1)
        
        return UISwipeActionsConfiguration(actions: [editAction, deleteAction])
    }
    
    // iOS11以降
    // 左から右へスワイプ
    @available(iOS 11.0, *)
    override func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
        
        let favoriteAction = UIContextualAction(style: .normal,
                                                title: "Favorite",
                                                handler: { (action: UIContextualAction, view: UIView, completion: (Bool) -> Void) in
            print("Favorite")
            // 処理を実行完了した場合はtrue
            completion(true)
        })
        favoriteAction.backgroundColor = UIColor(red: 210/255.0, green: 82/255.0, blue: 127/255.0, alpha: 1)
        favoriteAction.image = UIImage(named: "ic_favorite")
        
        return UISwipeActionsConfiguration(actions: [favoriteAction])
    }
}

『マジ文章書けないんだけど~朝日新聞ベテラン校閲記者が教える一生モノの文章術~』

www.daiwashobo.co.jp

書店員の友人から「タイトルの割にしっかりした内容で、ためになる」とおすすめされた本。
「マジ文章書けないんだけど」と言うほどひどくはない、…と思いたいけれど、
私も文章が上手く書けないと悩むことが多いので読んでみました。

内容の紹介

就活を控えた女子大生・すずが、文章の達人である謎のおじさんに文章の書き方を教えてもらうストーリー仕立てになっています。
最初は簡単な文法の説明から、だんだんと文章の膨らませ方、エントリーシートの書き方へと進みます。
就活生向けの内容と思いきや、就活に限定した内容は最後の15ページほどなので、意外と万人向けでした。
くだけた会話調で書かれ、可愛いイラストも多いので、本を読むのが苦手という人には読みやすいと思います。

この本が紹介する「文章術」の半分は、

  • 助詞の「は」と「が」の違い
  • 主語と述語を対応させる
  • こそあど言葉

など、とても基本的な文法の説明です。

もう半分は

  • 一つの文に一つの内容
  • 5W1H、特にWhyが重要
  • 結論から先に書く

など、これもまた一度は聞いたことがあるような基本的な内容がほとんど。
タイトル通り「マジ文章書けないんだけど」な人にわかりやすい文章の書き方、書き始め方を教える本なので、ある程度勉強している人には物足りないかもしれません。

私もほとんど聞いたことのある内容ではありましたが、改めて振り返ると結構できていないもので。
文筆のプロのような上手い文章が書けるテクニックの紹介を期待していたので、そういう意味では期待はずれでしたが、上部のテクニックや色気を出す前に、基礎基本をきちんとすることが大事ですね。

この本の良いところは、本当にありそうなダメな文章の例を出して、ダメな原因と改善例が説明されているところです。

例えば
「このバックはブランドものなので、値段と人気が高く、品質とデザインが美しいブランドだ」
という文例。
自分もこんな文を書いている、ダメなのはわかるが何がダメでどう直せばいいのか説明できないという人には一読の価値ありです。


個人的に特に参考になったのは以下のアドバイスです。

  • 一つの文に一つの内容
  • 「だが」と「ので」などのつなぎ言葉はできるだけ使わない

私は一つの文にいろいろ詰め込んで文が長くなりがちで、結局何が言いたいかが迷子になることがよくあります。
この本を読んでから、意識的に文を短くシンプルにするようになり、少し良くなった実感があります。
文を短くした方が書くのも早いですね。

書かれている内容が根本的であるだけに実践しやすく、普遍的に使える”一生モノの文章術”だと思いました。

【Xcode】XCUITest + fastlane/snapshotで始めるUIテスト(後編)

後編ではfastlane/snapshotを導入して、任意のタイミングでスクリーンショットを取得します。

前編はこちらから。
【Xcode】XCUITest + fastlane/snapshotで始めるUIテスト(前編) - 文系プログラマの勉強ノート


fastlane/snapshotとは

github.com

fastlaneは、iOS/Androidアプリのリリース作業を自動化するツール群です。
そのうちの一つであるsnapshotを使うと、UIテストをしながらストア申請時のスクリーンショットを自動作成できます。

開発環境

  • Mac OS Sierra
  • Xcode8.3
  • Swift3

導入手順

1.Xcode command line toolsをインストールする

ターミナルを起動し、Xcode command line toolsをインストールします

$ xcode-select --install
2.fastlaneをインストールする

ターミナルからRubygemsを使ってインストールします。
この他、Homebrew、Zipで入れる方法もあります

$ sudo gem update
$ sudo gem install fastlane -NV
3.snapshotを初期化する

snapshotを取りたいアプリの .xcodeproj ファイルがあるディレクトリに移動し、下記のコマンドを実行します。

$ cd /Users/xxx/XCUITestSample
$ fastlane snapshot init
$ sudo gem install fastlane -NV

下記のファイルが生成されます。

  • Snapfile
  • SnapshotHelper.swift
4.SnapshotHelper.swiftをプロジェクトに追加する

Xcodeを起動し、生成されたSnapshotHelper.swift をUIテストターゲットに追加します。
SnapshotHelper.swift は移動可なので、私はファイル本体も UIテストディレクトリ以下に移動しました。
f:id:an3714106:20170702105701p:plain

5.テストコードを編集する

前編で作成した ***UITests.swift の setUp() を修正します。

//XCUIApplication().launch()
let app = XCUIApplication()
setupSnapshot(app)
app.launch()

UIテストコード(ここではtestExample())のスクリーンショットを取得したい箇所に下記のコードを追加します。
""の文字列は任意の文字列です。画像ファイル名に使用されますので、重複しないようにします。

snapshot("ScreenShot_01")
6.Snapfileを編集する

スクリーンショットを取得したい端末と言語をSnapfileに記載します。
以下は iPhone7 の日本語の場合です。

devices([
  "iPhone 7",
  "iPhone 7 Plus",
])

languages([
  "en-US",
  "ja-JP",
])
7.fastlane/snapshotを実行する

ターミナルから下記のコマンドでfastlane/snapshotを実行します。
(snapshotを取りたいアプリの .xcodeproj ファイルがあるディレクトリで実行してください)

$ fastlane snapshot

設定ファイルやテストコードに問題がなければ、自動的にシミュレータが起動し、テストが実行されます。
完了すると screenshots/screenshots.html が表示されます。
screenshots/screenshots.html には作成されたスクリーンショットが環境別に一覧になっています。
画像は screenshots ディレクトリ以下に保存されます。

公式ドキュメントによると、ストア申請用にデバイスフレームをつけたりもできるようです。

参考URL

fastlane docs

【Xcode】XCUITest + fastlane/snapshotで始めるUIテスト(前編)

前編では、XCUITestを導入してUITestを実行してみます。

XCUITestとは

Xcode7から追加されたUIテスト機能です。
UIテストをするためのフレームワークはEarlGreyやappiumなどもありますが、
iOS8以降対応で問題なければ、XCUITestが扱いやすいと思います。

特徴

  • Xcodeに統合されている
  • アプリケーションと同じ言語で記述できる(Swift/Objective-C)
  • テストステップ毎に自動でスクリーンショットが取得される
  • アプリを操作してテストコードを作成するRecording機能がある
  • wait/sleepを入れなくても、アニメーションの待ち時間などのタイムラグをある程度上手く処理してくれる

開発環境

  • Mac OS Sierra
  • Xcode8.3
  • Swift3

導入手順

1. UI Testing Bundleを追加する

UITestを追加したいプロジェクトを開き、メニューの File > New > Target... を選択します。

iOS > Test > "iOS UI Testing Bundle" を選択して Next を押します。
f:id:an3714106:20170606221031p:plain:w500

Product Name を入力して Finish を押します。

(新規プロジェクトの場合は、作成時にプロジェクト名などを入力する画面で "Inclure UI Tests" にチェックを入れればOK)

2. テスト対象となる機能を作成する(すでにある場合は次へ)

今回はテスト用なので、テキストフィールドに文字を入力して決定ボタンを押すと
入力内容がアラートで表示される機能にしました。
f:id:an3714106:20170611111252p:plain:w250

3. Accessibility identiferを設定する(すでにある場合は次へ)

テストコードからコントロールにアクセスするために、Accessibility identiferを設定します。
コードから追加する方法もありますが、ここではIBを開いてコントロールを選び、右側のペインの
Identity inspector(左から3番目) > Accessibility > identifer
に一意の文字列を設定します。
f:id:an3714106:20170611112328p:plain

※Accessibility identiferが保存されない場合※
稀にAccessibility identiferを設定しても、実行時に保存されていないことがあります。
その場合、Accessibility identifer設定後に、座標やサイズなどレイアウト情報を変更すると保存されました。
(おそらくXcodeのバグ。保存されたあとはレイアウト情報は戻して大丈夫です。)

4. テストコードを作成する

先程追加したUITestTarget内のswiftファイルにテストコードを書きます。
(デフォルトでは"[ProjectName]UITests.swift"というファイル名になっていると思います。)

初期状態で「testExample()」というメソッドが作成されていますが、
このように「test〜」で始まるメソッドがテスト時に実行されるコードになります。
f:id:an3714106:20170611113555p:plain

testExample()内にテストコードを作成します。
"textField"、"decisionButton"は先程設定したAccessibility identiferです。

func testExample() {

    let app = XCUIApplication()

    // 文字を入力
    let textField = app.textFields["textField"]
    textField.tap()
    textField.typeText("UIテストを実行")

    // 決定ボタンをタップ
    let button = app.buttons["decisionButton"]
    button.tap()

    // 入力内容がアラートに表示される
}
5. テストを実行する

メニューの Product > Test を選択するか、テストメソッド名の左側の◇ボタンを押してテストを実行します。
◇ボタンが緑色になれば成功、赤くなれば失敗です。
f:id:an3714106:20170611121054p:plain:w400

6. テスト結果を確認する

左側のペインの Report navigator(一番右) から実行したテストを選ぶと、テスト結果の詳細が表示されます。
f:id:an3714106:20170611124452p:plain

XCUITestは自動でスクリーンショットを生成してくれます。
▼ボタンを押してツリーを開き、目のアイコンを押すとスクリーンショットを確認できます。
f:id:an3714106:20170611124939p:plain
f:id:an3714106:20170611124950p:plain:w500

もし任意のタイミングでスクリーンショットを取得したい、スクリーンショットをまとめて見たいという場合は
fastlane/snapshotを併用することで実現できます。

というわけで、後編ではfastlane/snapshotを導入していきます。

【Xcode】コードだけでグラデーション作成

下図のようなグラデーションをコードだけで作る方法です。
f:id:an3714106:20170611133939p:plain:w250

override func viewDidLoad() {
    super.viewDidLoad()

    let colors = [UIColor(red: 112/255, green: 134/255, blue: 241/255, alpha: 1.0).cgColor,
                  UIColor(red: 40/255, green: 169/255, blue: 255/255, alpha: 1.0).cgColor,
                  UIColor(red: 180/255, green: 225/255, blue: 255/255, alpha: 1.0).cgColor]
    let gradientLayer = gradientLayerWith(frame: view.bounds, colors: colors)
    view.layer.addSublayer(gradientLayer)
}

func gradientLayerWith(frame: CGRect, colors: Array<Any>) -> CAGradientLayer {

    let layer = CAGradientLayer()
    layer.frame = frame
    layer.colors = colors

    // グラデーションの向きを横にしたい場合
    layer.startPoint = CGPoint(x: 0, y: 0.5)
    layer.endPoint = CGPoint(x: 1, y: 0.5)

    return layer
}

【Xcode】UIAlertControllerで簡単進捗ダイアログ作成

カスタムビューを作らず、UIAlertControllerで簡単な進捗ダイアログを表示する方法です。

f:id:an3714106:20170213234543p:plain

// インジケータ表示
alert = UIAlertController(title: "Loading...", message: "\n", preferredStyle: .alert)

let indicator = UIActivityIndicatorView()
indicator.translatesAutoresizingMaskIntoConstraints = false
alert.view.addSubview(indicator)

let views: [String: UIView] = ["alert": alert.view, "indicator": indicator]
var constraints = NSLayoutConstraint.constraints(withVisualFormat: "V:[indicator]-(12)-|",
                                                 options: [],
                                                 metrics: nil,
                                                 views: views)
constraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|[indicator]|",
                                              options: [],
                                              metrics: nil,
                                              views: views)
alert.view.addConstraints(constraints)

indicator.isUserInteractionEnabled = false
indicator.color = UIColor.lightGray
indicator.startAnimating()

present(alert, animated: true, completion: {
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
        self.alert.dismiss(animated: true, completion: {
            self.alert = nil
        })
    }
})


こちらのサイトの回答を参考にSwift3.0の文法で書き直し、インジケータの位置を調整しました。
アラートの高さをmessageの改行で調整しているのが少々ださいですが、
カスタムビューを作らなくて良いのでお手軽ですね。
stackoverflow.com