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

iOSDC Japan 2017に行ってきました

子どもができてからというもの、なかなか勉強会やイベントに参加できなくなっていましたが、久々に参加してきました。

iosdc.jp

かなり悩みつつセッションの選択をしたなかで、特に印象に残ったものをあげます。

両OSやるマンという選択

speakerdeck.com

実際に1人でiOS/Android対応されている方の発表。

意外とAndroidも開発してるよって人が多かったのが印象的でした。

違いを意識してプロダクトに落とし込むことが一番重要っていう部分は同意しかなくて、ディレクターさんやデザイナーさんにこそ共有したいなーと思いました。

Android開発もやってみよっかなーという気分になりました。

アプリエンジニアはどのように事業に貢献すべきか

speakerdeck.com

近々、初めてBtoCのサービス開発に関わる予定なのですが、同じようなことに悩むんだろうかと思いつつ聞いていました。

気になったキーワード

  • アプリを使うことがユーザーにとってやりたいことではない
  • ユーザーが喜ぶ体験を理解して、アプリ上で実現する
  • アプリ以外も含めてサービスでありユーザー体験である

何度か読んで、自分なりの考えをまとめてみるつもり。

結婚式を支えた技術 Firebaseを活用したサーバレスiOSアプリケーション開発

speakerdeck.com

結婚式の写真を共有してもらうために1ヶ月でアプリを作ってしまうという素敵なお話。

身近なところにある問題を解決するのは、ユーザーも近くてフィードバックもらいやすいし、なにより解決すれば喜んでもらえるし、いいことずくめですね。

我が家でも、妻からこんなアプリできないの?みたいなことをたまに言われるのですが、子どもと遊ぶのを言い訳にしてしまっていて反省。

とりあえずFirebase触ってみようと思いました。

全体通しての感想

めちゃめちゃ楽しかったです。

以下、楽しかったことを雑にまとめます。

  • オープニングから力の入ったムービーでテンションあがる
  • セッションはどれも面白く、同じ枠で聞きたいものがかぶりまくって泣く泣く選択するという贅沢な状況
  • 懇親会でもないのにビールが配られて、飲みながら聞くLT最高
  • 運営の方々が、ほんとによく動いていて、とにかくスムーズな運営が印象的

ただ、2日目の途中で体調を崩してしまい、途中で帰らざるを得なくなってしまったのが心残りです。

来年も開催されたら、最後まで参加するぞ。懇親会も参加できたらいいな。

Swiftはプロパティもオーバーライドできる

バージョン

  • Swift 3.0.2
  • Xcode 8.2.1

クラスのプロパティ

知らなかっただけなのですが、Swiftはプロパティもオーバーライドできます。

使い方次第で便利に使えそう。

サンプルコード

プロパティのオーバーライドとプロパティ監視を組み合わせて、ボタンが無効化されたらアルファ値を変えるコードです。

import UIKit

class CustomButton: UIButton {
    override var isEnabled: Bool {
        didSet {
            super.isEnabled = isEnabled
            self.alpha = isEnabled ? 1.0 : 0.4
        }
    }
}

let customButton: CustomButton = CustomButton()

print(customButton.isEnabled) // true
print(customButton.alpha) // 1.0
customButton.isEnabled = !customButton.isEnabled

print(customButton.isEnabled) // false
print(customButton.alpha) // 0.4

UIViewからUITableViewのカスタムセルを作るとボタンタップイベントが取れない

現象

UITableVIewのカスタムセルにボタンを置いて、タップ用の処理を書いたがタップイベントを検知できない。

原因

ボタンの上のレイヤーに、UITableViewCellContentView というビューが描画されており、このビューに邪魔されてタップイベントが検知できていなかったため。

f:id:y_sumida:20170301094905p:plain

根本原因

カスタムセルのxibファイルを、UITableViewCell ではなく、 UIView を使って作成していたため。

UITableViewCell で作ったxibは、UITableViewCellContentView の上に自分の配置したパーツが描画されています。

f:id:y_sumida:20170301094946p:plain

ちなみに、xibファイルの新規作成すると、デフォルトで UIView が置いてあるというXCodeのおせっかい機能のがほんとの原因かもしれません。

f:id:y_sumida:20170301095134p:plain

対策

カスタムセルを作る際には、xibファイルのデフォルトで置いてある UIView を消して、UITableViewCell を置くと良いです。

f:id:y_sumida:20170301095039p:plain

または、新規ファイルを作成する際に、クラスファイルから作成して、 Cocoa Touch Class を選択して、次の画面で Also create XIB file にチェックを入れると、勝手に UITableViewCell を配置したxibファイルを生成してくれます。

f:id:y_sumida:20170301095245p:plain

f:id:y_sumida:20170301095341p:plain

AutoLayoutに慣れてる人には当たり前なのかもしれないですが、ちょっとわかりにくいなーと思いました。