画像とラベルを縦に並べるUIButtonのカスタムクラス

はじめに

たまに必要になるので、こんな感じに画像とラベルを縦に並べるUIButtonのカスタムクラスを作りました。

f:id:y_sumida:20180122225341p:plain

contentEdgeInsetsとか毎回調整するのめんどくさいですよね。

探せばライブラリとかもあるとは思うけど勉強も兼ねて車輪の再発明です。

コード

github.com

画像とラベルのサイズに合わせて、縦に並べてセンタリングします。

Storyboardからもコードからも使えるようになってます。

コードからは上下と左右のマージンが指定できます。

不具合

  • width指定、leadingEdge/trailingEdge指定などで、必要な幅より狭い幅の場合に表示が崩れる。
    • いろいろ試したけど、今のところうまくいってない。
    • drawに入ってきた時点でラベルの後ろが切れてたりするので。

感想など

  • もうちょっとスマートに書きたかった。
  • contentEdgeInsetsとかの親クラスの変数を変更不可にするのに手間取った。
  • UIButtonに縦並びとかアイコンとラベル入れ替えるオプション欲しい。

参考

UIViewのライフサイクルについて、以下の記事が大変参考になりました。

developers.eure.jp

2017年振り返り

個人の日記なので2017年の振り返り。

お仕事

ご縁があって11月から渋谷にある事業会社でiOSアプリ開発者として働いている。

春先、前年から開発していたiOSアプリのリリース延期が決まりモチベーションが下がっていたところに、今の会社の人に声をかけてもらい、会社見学やコード課題などを経て働くことになった。

2ヶ月経過した感想としては、今のところ概ね楽しく働けている。

完璧な職場なんてあるはずもないけど、月の残業時間が10時間未満だったり、希望したREALFORCEが特に何も言われることもなく出社時に用意されている程度にはホワイトな職場。

子持ちの自分にとっては比較的自由な勤務体系なのも嬉しいところ。

コードレビューやCI環境なども整っていたり改善が進んでいたりしていて学ぶことが多い。

少人数なチームだけど優秀な人ばかりなので、自分もがんばらねばと思っている。

勉強会など

久しぶりにiOSDCという大きめなイベントに参加して多くの刺激をもらった。

子どもがいるとなかなか参加しにくいけど厳選して参加していこうと思う。

プライベート

"Write Code Every Day" を実践しようと思い立ってやってみた。

仕事のコード除くとこんな感じ。後半息切れ気味な感は否めない。

f:id:y_sumida:20171231152722p:plain

github.com

結果としては毎日というわけにはいかなかったけど、日々ちょっとでもコードを書く、エディタに向かうという習慣はできた気がする。

どうしても作りたいものがあってプログラミングしている人間ではないのでサンプルコードや検証コードがほとんどだけど、転職の際にも多少は役に立ってくれた。

やってみてわかったのは、時間がない中でやると圧倒的にインプットが減るということ。

子どもたちを寝かしつけた後の22時〜24時の最大2時間程度が貴重な勉強時間なので、バランス良くインプット・アウトプットができるように改善したい。

立てていた目標

  • Qiita:Teamをディレクターさん、デザイナーさんも使うようになってきたのでそっち向けの記事も書く
    • 前職で多職種向けの記事を書いたけど十分じゃなかった
  • Swift環境での自動テストを導入する
    • できなかった
  • iOSアプリ開発に偏った1年だったので、Web開発もうちょっとがんばる
    • できなかった
  • 何かしらアウトプットする
    • "Write Code Every Day"を不完全ながら実践

トータルで見ると、あんまり進捗よくなかった。無念。

来年の目標

  • Androidで何か作ってみる
  • UIKitの知識を深める
  • インプットを増やしてインプット・アウトプットのバランスを改善する

たくさん目標立ててもできないのでこんなもんで。

Swiftでクロージャが入れ子の場合のキャプチャリスト

はじめに

Swiftのクロージャには循環参照を回避するための仕組みとしてキャプチャリストがあります。

最近、キャプチャリストがあるのにメモリリークするというバグに遭遇して、自分の認識が間違っていたことが分かったのでメモしておきます。

結論としては、クロージャ入れ子の場合には、直接使っていなくても外側にキャプチャリストを書いておかないと強参照になります。

バージョン

Swift 4.0

サンプルコード

class Hoge {
    deinit {
        print("deinit")
    }
    let n: Int
    
    init(n: Int) {
       self.n = n
    }
    // 内側だけweak
    lazy var closure1: () -> Int = {
        return {[weak self] in return (self?.n)! * 2}()
    }
    // 外側だけweak
    lazy var closure2: () -> Int = { [weak self] in
        return {return (self?.n)! * 2}()
    }
    // 両方weak
    lazy var closure3: () -> Int = { [weak self] in
        return {[weak self] in return (self?.n)! * 2}()
    }
    // 内側だけunowned
    lazy var closure4: () -> Int = {
        return {[unowned self] in return self.n * 2}()
    }
    // 外側だけunowned
    lazy var closure5: () -> Int = { [unowned self] in
        return {return self.n * 2}()
    }
    // 両方unowned
    lazy var closure6: () -> Int = { [unowned self] in
        return {[unowned self] in return self.n * 2}()
    }
}


do {
    // deinit呼ばれない
    print("closure1")
    let hoge = Hoge(n: 100)
    print(hoge.closure1())
}

do {
    print("closure2")
    let hoge = Hoge(n: 100)
    print(hoge.closure2())
}

do {
    print("closure3")
    let hoge = Hoge(n: 100)
    print(hoge.closure3())
}

do {
    // deinit呼ばれない
    print("closure4")
    let hoge = Hoge(n: 100)
    print(hoge.closure4())
}

do {
    print("closure5")
    let hoge = Hoge(n: 100)
    print(hoge.closure5())
}

do {
    print("closure6")
    let hoge = Hoge(n: 100)
    print(hoge.closure6())
}

iOSで画像をリサイズして保存するときに気をつけること

はじめに

カメラロールからユーザーが選んだ画像をUIImageViewに表示してから、リサイズしてファイル保存する際に、ちょっとはまったのでメモ。

バージョン

  • Xcode9.0.1
  • Swift4.0

やりたかったこと

  • ユーザーが選択した画像ファイルを一定の幅にリサイズして保存しておき、非同期でアップロードする

起こった事象

  • iPhone SEだとアップロードできる画像が、iPhone Xだとサーバのファイルサイズ制限に引っかかった
  • ファイルを調べると、オリジナル画像よりもファイルサイズが大きくなっていた

コード

最終的にこんな感じのリサイズ処理にしました。

サンプルコードなので単純に縦横半分にしています。

    private func resize(image: UIImage) -> UIImage {
        // 縦横の画素数を半分にする
        let width = image.size.width * 0.5
        let height = image.size.height * 0.5
        // scale の設定が0だとオリジナル画像よりもサイズが大きくなるので1を設定
        UIGraphicsBeginImageContextWithOptions(CGSize(width: width, height: height), false, 1.0)
        image.draw(in: CGRect(x: 0, y: 0, width: width, height: height))
        let resizeImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()

        return resizeImage!
    }

原因

UIGraphicsBeginImageContextWithOptions(_ size: CGSize, _ opaque: Bool, _ scale: CGFloat)scale パラメータに0.0を指定すると、デバイスにあわせて@2/@3の指定にしてくれます。

The scale factor to apply to the bitmap. If you specify a value of 0.0, the scale factor is set to the scale factor of the device’s main screen.

UIGraphicsBeginImageContextWithOptions(_:_:_:) - UIKit | Apple Developer Documentation

その結果、iPhon Xだと@3の画像となり、結果としてファイルサイズが大きくなってしまっていたのでした。

アプリ埋め込みのアイコンなどはデバイスにあわせた画像を用意しますが、ユーザーの写真などは原寸でいいと判断して、scale を 1.0指定することで解決しました。

UIVIewController.viewの生成タイミング

Instantiateというライブラリがあります。

github.com

StoryboardやXibを使いつつコンストラクタインジェクションできる素敵ライブラリなのですが、コードを読んでてよくわからなかったところがありました。

Instantiate/Storyboard+UIViewController.swift at master · tarunon/Instantiate · GitHub

このコードの self.view を参照しているところ。 左辺が _ となっているので何もしてないように見えます。

            if self is ViewLoadBeforeInject {
                _ = self.view
            }

ViewLoadBeforeInject というのはプロトコルで、定義はこんな感じ。

/// This protocol allow viewController force load view before call `inject`
public protocol ViewLoadBeforeInject {
    
}

コメントを読むと、 UIViewController.viewinject が呼ばれる前に強制的にロードするもののようです。

inject はコンストラクタ内で呼ばれるメソッドです。

色々読んでるうちに UIViewContoroller.view のコメントにたどり着き理解できました。

    open var view: UIView! // The getter first invokes [self loadView] if the view hasn't been set yet. Subclasses must call super if they override the setter or getter.

先程のコードはコンストラクタ中のコードであり、最初のアクセスなのでviewを生成してるようです。

ゲッターにこんな副作用持たせてるとは思わなかった。

このコードのおかげで、inject メソッドで UIViewContoroller に置いてある UILabel とかをセットするような場合に、このプロトコルを適用すればクラッシュしません。

内部の変数に保持して ViewDidLoad とかでセットするなら、このプロトコルは不要です。

うまいことできてる。

祝日や会社の休日に起動しないSlack通知bot

はじめに

平日の18時に行っている夕会用に、開始時間になると曜日ごとのファシリテータをお知らせするbotを運用中。

土日祝日と会社指定の休日は通知飛ばないようにしてます。

祝日判定のためにGoogleAppsScriptとGoogleカレンダーを利用してます。

Googleカレンダーに用意されている日本の祝日カレンダーで祝日を、別に用意した会社の休日カレンダーで年末年始や夏季休暇に対応しています。

コード

function onOpen() {
  if (isHoliday()) {
    return
  }
  deleteAllTriggers();
  setupTriggers(); 
}

function deleteAllTriggers() {
  var allTriggers = ScriptApp.getProjectTriggers();
  for(var i=0; i < allTriggers.length; i++) {
      ScriptApp.deleteTrigger(allTriggers[i]);
  }
}

function getFormatedDate(time){
  var now = new Date();
  var year = now.getFullYear();
  var month = now.getMonth() + 1;
  var date = now.getDate();

  month = ("0" + month).slice(-2);
  date = ("0" + date).slice(-2);

  var strDate = year + "/" + month + "/" + date + " " + time;

  return strDate;
}

function setupTriggers() {
  // 通知時間の設定
  var trigger = new Date(getFormatedDate("18:00"));
  var onChangeTrigger = ScriptApp.newTrigger("notfy")
                  .timeBased()
                  .at(trigger)
                  .create();
  //全トリガー削除するので再設定
  var onChangeTrigger = ScriptApp.newTrigger("onOpen")
      .timeBased()
      .atHour(10)
      .everyDays(1)
      .create();
}

function notfy() {
  var facilitator = getTodayFacilitator();

  postSlack(facilitator);
}

function isHoliday() {
  var japanHoliday = CalendarApp.getCalendarsByName('日本の祝日');
  var companyHoliday = CalendarApp.getCalendarsByName('会社の休日'); 
  var holidays = japanHoliday[0].getEventsForDay(new Date()) + companyHoliday[0].getEventsForDay(new Date()) ;

  return (holidays != '');
}

function getMembers()
{
  var values = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('member').getRange(2, 1, 5, 2).getValues();  
  var members = {'日':'', '月':'', '火':'', '水':'', '木':'', '金':'', '土':''};
 
  for (var i = 0; i < 5; i++) {
     var youbi = values[i][0];
     members[youbi] = values[i][1];
  }
  
  return members
}

function getTodayFacilitator() {

  var now = new Date();
  var week = ['日', '月', '火', '水', '木', '金', '土']; 
  var members = getMembers();

  var facilitator = members[week[now.getDay()]];

  return facilitator;
}

// send to slack
function postSlack(name)
{
  if (name == "") return;

  var payload = {
    "text" : "<!here> " + "今日の当番は" + name + "ですよ",
    "channel" : "#general" // 表示名やアイコンはお好みで
  }

  var options = {
    "method" : "POST",
    "payload" : JSON.stringify(payload)
  }
 
  var url = "https://hooks.slack.com/services/hoge/fuga/piyo"; // 各自のURL
  var response = UrlFetchApp.fetch(url, options);
  var content  = response.getContentText("UTF-8");
}

ポイント

使うまで知らなかったけど、GoogleAppsScriptの時間起動トリガーだと毎日18時っていう指定ができませんでした。

近いのは、毎日18時〜19時の間に起動するというトリガーか特定日の18時というトリガーです。

仕方ないので、毎日10時〜11時の間に起動して、setupTriggers()という関数でその日の18時のトリガーを設定してます。

さらに面倒なことにトリガーを区別するのが大変ぽかったので、一回全部消してから全部設定し直すようにしてます。

Linux上で特殊文字を含む名前のファイルの削除方法

はじめに

コマンドの操作ミスやスクリプト出力の文字化けなどで特殊文字を含む名前でファイルができてしまい、単にrmコマンドでは削除できない時の削除方法。

いざという時のためにメモ。

実験用のファイル

temp $ ls
"ccc.txt    -bbb.txt    aaa.txt
temp $

他にもありえますが用意するのがめんどくさかったのと、今回の削除方法はファイル名によらず使えるのでこれで。

実験

とりあえず用意したファイルを普通に消そうとするとどうなるか見てみます。

先頭がダブルクォートのファイル名

temp $ rm "ccc.txt
>
temp $

次の入力を待たれてしまいます。

先頭がハイフンのファイル名

temp $ rm -bbb.txt
rm: illegal option -- b
usage: rm [-f | -i] [-dPRrvW] file ...
       unlink file
temp $ 

rmコマンドのオプションと解釈されてしまいます。

解決方法

いろいろなやり方があるのですが、汎用的な方法を。

手順1 ls -iでinode番号を確認

temp $ ls -i
2991451 "ccc.txt    2991471 -bbb.txt    2991475 aaa.txt
temp $

ファイル名の前に表示されているのがinode番号です。

手順2 確認したinode番号を指定して削除

temp $ rm `find . -inum 2991451`
temp $ ls
-bbb.txt    aaa.txt
temp $ rm `find . -inum 2991471`
temp $ ls
aaa.txt
temp $

削除できました。

実際にやる場合は、事前の削除対象の確認を忘れずに。

おまけ

実験用に使ったパターンのものは別の方法でも削除できます。

先頭がダブルクォートのファイル名

2つ方法があります。

  • ファイル名のダブルクォートをエスケープする
    • rm ¥"ccc.txt
  • ファイル名をダブルクォートで囲んでファイル名のダブルクォートをエスケープする
    • rm "¥"ccc.txt"

先頭がハイフンのファイル名

こちらは、ダブルクォートの方法では消せないので以下の方法を使います。

  • コマンドのオプションに--を指定する
    • rm -- -bbb.txt