文字っぽいの。

文字を書いています。写真も混ざります。

Sora iOS SDKがちゃんと導入されているのに Module 'Sora' has no member named 'hoge' となる問題の解決法

import Sora

struct Test {
    init() {
        Sora.shared.audioEnabled = false
    }
}

と書くと使えるはずなんですが、Module 'Sora' has no member named 'shared' とエラーが出てしまいます。これを直すのは簡単で、

f:id:FromAtom:20190212135426p:plain

こういう感じにSimulatorではなくて

f:id:FromAtom:20190212135519p:plain

こうする。もしくは、実機に向けてビルドしてあげればよい。 

sora.shiguredo.jp

にはちゃんと (Sora iOS SDK はシミュレーターに対応していません) と書かれているが、見逃していた。アプリを作る場合、最初はSimulatorを使いながらあれこれ実装していくとおもうので、お気をつけください。

マネジメントとエンジニアリングの両立が難しいのはなぜか?のたとえ話

与太話です。

数ヶ月前、マネジメントをしながらメイン機能の開発をしていたら完全にタスクがオーバーフローして大変なことになったんですよ。 その時に「両立が難しいのは分かったが、なぜ両立が難しいんだろうか。」をひたすら考えていたら思いついた「たとえ話」です。

素潜り漁師と漁船で例えます。

エンジニアリングは素潜り漁

  • コードを書いているときは集中して潜りきらないといけない
    • エンジニアが割り込みを嫌うのはこのため
    • 途中で呼び止められたら潜水をやめて浮上しないといけない
    • 割り込みが終わったらまた潜り始める
  • 難しい機能を作る時ほど深く潜らないといけないというイメージ
  • 深さと釣果には相関がないので深ければ大漁というわけではない
  • ただ、深い漁場は素人の素潜り漁師が到達できないので、漁場が荒らされていなくて釣果が期待できる
  • 浅めの場所を複数箇所どんどん潜って数を稼ぐタイプがいる
  • 深い場所に潜り続けて「あの海産物なら奴だ」と言わせるタイプもいる

マネジメントは漁船上にいる船長

  • 漁船の上でレーダー・天候・海の様子をみたり、他の船や漁港に無線連絡したりする
  • 船上で釣果を見ながら「ここは獲れなさそうだから移動するぞ」などとする
  • 漁の終了などの管理もする
  • 急なシケや船の接近に備えて注意を払う
  • 失敗すると素潜り漁師が死ぬ or 釣果が振るわずに飯が食えなくなるので責任が大きい
  • とにかく色んな状態・状況・様子を俯瞰して見続けないといけない

  • 担当しなければならない視点・視座・視野がぜんぜん違う
  • なのでこれを同時にやるのは非常に難しい
  • 両立できる人は少ない
    • マネジメントは小さく始まるので、みんな最初は両立できると思いこんでいる
    • だんだんとエンジニアリングに割ける時間が減っていったり、逆にマネジメント系の仕事を受けないように整理していく
  • できる人はいるか
    • 複数サービスにまたがって開発している時に、コンテキストスイッチが速攻で切り替えられる人
    • 定時内はマネジメント、定時後はエンジニアリングのように力強いした働き方ができる人

iOS 12以降のAPIで "NSKeyedArchiver" と "NSKeyedUnarchiver" を使う

環境

  • Swift: version 4.2.1

やりたいこと

NSKeyedArchiverNSKeyedUnarchiver を使って、UserDefaultsやKeyChainに色々入れたり出したりすることがあると思います。iOS 11まではこうやって書いていました。

// ArchiveしてUserDefaultにセット
let rootObject: [Int] = [1, 2, 3, 4, 5]
let archivedData = NSKeyedArchiver.archivedData(withRootObject: rootObject)
UserDefaults.standard.set(archivedData, forKey: "key")

// UserDefaultからゲットしてUnarchiveする
let data = UserDefaults.standard.data(forKey: "key")!
let unarchivedObject = NSKeyedUnarchiver.unarchiveObject(with: data) as? [Int]

print(unarchivedObject) // => Optional([1, 2, 3, 4, 5])

ここで使っている archivedData(withRootObject:)unarchiveObject(with:)iOS 12以降のAPIではDeprecatedになっているので、置き換えます。

// ArchiveしてUserDefaultにセット
let rootObject: [Int] = [1, 2, 3, 4, 5]
let archivedData = try! NSKeyedArchiver.archivedData(withRootObject: rootObject, requiringSecureCoding: false)
UserDefaults.standard.set(archivedData, forKey: "key")

// UserDefaultからゲットしてUnarchiveする
let data = UserDefaults.standard.data(forKey: "key")!
let unarchivedObject = try! NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? [Int]

print(unarchivedObject) // => Optional([1, 2, 3, 4, 5])

Xcodeからは

'unarchiveObject(with:)' was deprecated in iOS 12.0: Use +unarchivedObjectOfClass:fromData:error: instead

というWarningが出るんですが、素直に unarchivedObjectOfClass(fromData:, error:)を使うと

let unarchivedObject = try! NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSArray.self], from: data) as? [Int]

というコードを書く必要があり(ArrayじゃなくてNSArrayです)難しさが増すので unarchiveTopLevelObjectWithData()を使うのをおすすめします。

「いい温泉に入る」を目的としたストイック家族旅行に行ってきました。

背景

嫁の友人4人が泊りがけで我が家に遊びに来ることに。コミュ障な旦那は家から逃げ出すため、弟を召喚して温泉旅行を計画。せっかくだからと両親も誘って家族旅行にした。

takachi.hatenablog.jp

この記事をよんで「山梨ええやん」となったので、

  • 日帰り温泉を旅のメインとする
  • 宿はビジネスホテルで安く済ませる
  • 良質な温泉が主目的だから観光はプランに入れない(偶然通りがかったら寄る程度)

というストイックな家族旅行プランが立案・施行された。

1日目

甲府駅

新宿駅から1時間半ほど特急あずさに乗って到着。初めて降りた。

f:id:FromAtom:20190105114306j:plain

クソデカ武田信玄が迎えてくれる。

ここから両親が乗ってきた車と合流する。ここから解散までは車移動。 自分で運転したかったので、コンビニで1日保険に加入した。

奥藤本店 甲府駅前店

昼飯はほうとうを食べに。ぶっちゃけほうとうってそんなに好きじゃないんだけど、 観光なのでまぁそれっぽいものを食べましょうということで。

f:id:FromAtom:20190105121502j:plainf:id:FromAtom:20190105121237j:plain

鶏のもつ煮も有名らしいので、弟はそちらを注文。「うん、ほうとうですね。」という味でした。

奈良田温泉 白根館

ほうとうを食べた後は主目的の温泉へ。甲府駅から車で1時間半ほど走った山奥にある。

エッサホイサと運転してようやく到着。車から降りると硫黄の匂いに包まれる。

f:id:FromAtom:20190105154827j:plain

早速温泉に向かう。入湯料は1人1000円。

f:id:FromAtom:20190105152218j:plain

は〜〜〜〜最高かよ〜〜〜〜〜。もちろん源泉かけ流しなので無限にお湯が生成されており最高。 ヌルヌルするというかもうヌッルンヌッルン。あまりにも最高なので最高だった。 1時間半の山道ドライブの大変さなんてすべて吹っ飛ぶ非常に価値がある温泉。

ちなみに飲むこともできます。

f:id:FromAtom:20190105152343j:plain

硫黄泉なので「わー……すっごい……」という味。

注意点としては、

  • 日帰り入浴の受付時間は14:00〜15:30で16:00には終了
  • 入浴客が多い場合は受付で断られることもある
  • 受付開始時間に到着できるように旅程を組むと良い

という感じ。小さな温泉宿なので、「日帰り入浴のお客さんが多すぎて、宿泊のお客さんが温泉を楽しめない」ということがないように配慮しているんですね。

夕食・宿

日帰りでいける温泉がメインの旅行なので宿はアパホテル。朝食付きツインを2部屋とって、父・母と俺・弟で泊まった。値段も1人5000円弱で安く済む。安いのは便利なので便利。 チェックインをして部屋に入ったら、快適に過ごすための環境整備をした。

f:id:FromAtom:20190105174649j:plain

ホテルのテレビでSwitchのゲームをしたい場合に純正のドックを持っていくとかさばるので、こういうものがあるといい(弟所有品)

あとは

Fire TV Stick

Fire TV Stick

などがあれば、アニメや映画を快適に視聴できる環境が完成する。便利な時代になったもんだなぁ。 一通り環境整備が終わったら、ホテル近くの居酒屋で夕食。飲んだり食べたりした。

f:id:FromAtom:20190105181741j:plain

ホテルに戻ったらコンビニで買ったお酒を飲みながら、ひたすら弟とスマブラSPをしてから寝た。

2日目

朝です。朝食付きなので "THE ホテル朝食" を食べます。

f:id:FromAtom:20190106081225j:plain

ビジネスホテルの朝食っていいよね。特に当日に仕事がない時に食べるホテル朝食が好き。

山口温泉

2日目は山口温泉へ。めっちゃいい雰囲気の体重計がある。

f:id:FromAtom:20190106102924j:plain

入湯料は600円。かけ流し、加温・加水なしのお湯がアホみたいなにジャブジャブ湧いている。

f:id:FromAtom:20190106103057j:plain

この源泉はとてもぬるくて、冬だと露天に1度入ると外気が寒くて出られなくなる。 「はー、あったまったわー」という感覚は得られないけれど、温泉効果で湯上がりに服を着るとポカポカと体が暖かい。不思議なものだ。

湯治のための温泉なので、のんびりと1時間〜2時間お湯につかるのがよい。ぬるいお湯なので全然のぼせないので無限に入っていられる。 イメージとしてはスーパー銭湯とかにある "つぼ湯" みたいな感じ。

お風呂上がりは牛乳を。

f:id:FromAtom:20190106112743j:plain

昼食

山口温泉を堪能したら帰路につく。途中のパーキングエリアで昼食。

f:id:FromAtom:20190106122847j:plain

こういうパーキングエリアの食券買うタイプのラーメンとか定食っていいよね。

解散

八王子駅で解散。おしまい。

まとめ

「いい温泉に入る」ということにフォーカスしたら、満足度の高い旅行ができた。今回訪れた、奈良田温泉 白根館、山口温泉ともに非常に良いお湯を堪能することができた。両親も温泉に満足してくれていたようで一安心。

他にも行きたい温泉はいくつもあるので、折を見てストイック旅行をしたいと思った。

2018年に買ってよかったもの

年末ですね。

BOSE QuietComfort 35 wireless headphones Ⅱ

ノイズキャンセリング・ヘッドホンが欲しくて買った。SONYと悩んだけどBOSE好きなのでBOSEにした。 ピュアなオーディオが好きな人はSONYで、作られたけど聞きやすい音が好きな人はBOSEが良い。 これはもう味の好き嫌いとか宗派の話になってくるので見た目とかで適当に決めれば良いんじゃないかな。

とにかく静かになる。定時後のオフィスでよく飲み会やゲーム大会が発生するんだけど、そういう場合でも問題なく作業ができる。 通常業務時間でもエアコンの駆動音とかが消えるので、普段地味にうるさかったことに気がつける。とにかく集中できて最高。

Logicool MX1600sGR ANYWHERE 2S

10年前くらいからロジクールのAnywhereシリーズを使っていて、ちょっと高いけどこれ以外のマウスは使う予定がない。 Darkfieldレーザーセンサーの力でガラスのテーブルだろうと膝の上だろうと、とにかくどこでも安定して使えるのが良い。 あと昔のモデルから着実に機能がアップしているのが良い。このバージョンだと

  • USBで充電できるようになった(前まで電池式だった)
  • ソフトをいれると最大3台のPC間でマウスカーソルを共有できる

という進化が発生していて最強だった。仕事でWindowsMacを行き来する人とかは使うと便利かと。マウスとしての性能は完璧。

Anker SoundCore Pro+

ポータブルスピーカー。値段的にはこっち↓のほうがお手頃なのでこっち使ってる人が多そう。

しっかりとした音が出てよい。そこらへんの下手なスピーカーより良いんじゃないかな。 なおかつこいつ自身がモバイルバッテリーになっていて、iPhoneを充電することができる。 基本的には家で使っていて、キッチンで動画を見るときに使っている。 防水なのでキッチンでも安心だし、バッテリーもかなり持つ。

難点としてはちょっと大きめで重い点ぐらい。

長ザル&ボウルセット

地味に便利。まな板の向こう側に置けるので、取り回しがめちゃくちゃしやすい。きゅうりやアスパラガスも入れられるのが便利。

綾鷹 茶葉のあまみ

めっちゃうまい。箱買いした。めっちゃうまい。

Nature Remo mini

Nature Remo mini 家電コントロ-ラ- REMO2W1

Nature Remo mini 家電コントロ-ラ- REMO2W1

Nature Remo出てて気になって居たけど高くて躊躇していたところに、これが発売されたので購入。 弊家は猫を飼っているので、夏の間はこれで室温を監視して、暑くなりすぎていたらエアコンをつけたり消したりしていた。 ずっとつけっぱなしだと寒くなりすぎることもあるので不安だったけど、こいつのおかげで安心して日々を過ごすことができた。

ウタマロ クリーナー

ウタマロ クリーナー 400ml

ウタマロ クリーナー 400ml

お風呂、キッチンのシンク、コンロがこれ1本ですべて掃除できる。 手荒れもしないし汚れもスイスイ落ちるので最強の洗剤だと思っている。

スコッチ・ブライト ハイブリッドネットスポンジ

ちょうどいい固さ、しなり具合なので、すこし力を入れて汚れを落としたいときにすごい安心感がある。もちろん汚れも落ちやすい。 すぐヘタってしまうスポンジもあるけど、こいつはずっといい感じの固さなので便利。

OKR

OKR(オーケーアール) シリコンバレー式で大胆な目標を達成する方法

OKR(オーケーアール) シリコンバレー式で大胆な目標を達成する方法

急に意識が高まったので買った。 これによってすべてが解決するわけではないけど、一定のフレームワークがあるっていうのは便利。守破離

いちばんおいしい家カレーをつくる

いちばんおいしい家カレーをつくる

いちばんおいしい家カレーをつくる

このレシピ通りにファイナルカレーを作ったら、めちゃくちゃ旨かった。スパイスからでカレー作るの楽しい。

たらこスプレッド

kaldi-online.com

美味しい。明太子パスタでも、パンに塗ってトーストでもOK。

麻辣ペッパー

kaldi-online.com

中華風の料理が食べたかったら、とにかくこれをゴリゴリやれば良い。キャベツとネギを炒めて鶏ガラスープとこいつをゴリゴリすればOKなので便利。ちょっと辛い。

ハバネロペッパーソース ファイアリーホット

kaldi-online.com

タバスコを使いたい場面でこれを使う。なお、次の日にアスホーがバーニングする。

それでは良いお年を。

Storyboardを利用したViewControllerのインスタンス生成を楽にする

背景

1つのStoryboardに1つのViewControllerという運用をしていると、ViewControllerのインスタンス生成するコードを書くことが多くなると思います。雑に書くと、

let storyboard = UIStoryboard(name: "SomeViewController", bundle: .main)
let vc = storyboard.instantiateInitialViewController() as! SomeViewController
present(vc, animated: true, completion: nil)

こんな感じでViewControllerを作ってモーダル表示できますね。

さて、これだと生成する度にこのコードを書かないといけないので大変です。なので生成したいViewController側に書いちゃいましょう。

final class SomeViewController: UIViewController {
    
    class func viewController() -> SomeViewController {
        let storyboard = UIStoryboard(name: "SomeViewController", bundle: .main)
        let viewController = storyboard.instantiateInitialViewController() as! SomeViewController

        return viewController
    }

}

...

// 使う時
let vc = SomeViewController.viewController()
present(vc, animated: true, completion: nil)

1回定義してしまえば使う時はさっと使えて便利です。実装も SomeViewController 側にまとめられるので安心。

ただ、この方法だとViewControllerを作る度に class func を作る必要があってわりと手間です。また、よくある事故として UIStoryboard(name: "SomeViewController", bundle: .main) のnameを変え忘れ問題があります。めんどくさいので書かなくても済むようにしたいです。

解決法

まずは次のような protocol-extension を書きます。

protocol Instantiatable {
    static var storyboardName: String { get }
}

extension Instantiatable where Self: UIViewController {
    static var storyboardName: String {
        return ""
    }

    private static var _storyboardName: String {
        if storyboardName.isEmpty {
            return className
        } else {
            return storyboardName
        }
    }

    private static var storyboard: UIStoryboard {
        return UIStoryboard.init(name: _storyboardName, bundle: nil)
    }

    private static var className: String {
        return String(describing: Self.self)
    }

    static func instantiateFromStoryboard() -> Self {
        guard let vc = storyboard.instantiateInitialViewController() as? Self else {
            fatalError("Can no instantiate \(Self.className) from \(storyboardName).storyboard")
        }
        return vc
    }

    static func instantiateFromStoryboard(withIdentifier id: String) -> Self {
        guard let vc = storyboard.instantiateViewController(withIdentifier: id) as? Self else {
            fatalError("Can no instantiate \(Self.className) from \(storyboardName).storyboard with id: \(id)")
        }
        return vc
    }
}

あとは自作のViewControllerで、

final class SomeViewController: UIViewController, Instantiatable {
// ...
}

とすれば

let vc = SomeViewController.instantiateFromStoryboard()
present(vc, animated: true, completion: nil)

と使うことができます。簡単ですね。もし、Storyboard IDを元にしたいのであれば

let vc = SomeViewController.instantiateFromStoryboard(withIdentifier: "SomeID")

とすればよいです。また、.storyboardの名前がViewControllerのクラス名と異なっている(今回の例の場合は SomeViewController.storyboard ではない)場合には

final class SomeViewController: UIViewController, Instantiatable {
    static let storyboardName = "OtherStoryboardName"

    //...
}

のように storyboardName を指定すれば良いです。この場合は SomeViewController.storyboardではなく OtherStoryboardName.storyboard を見に行きます。

まとめ

これで Instantiatable protocolを実装すれば、追加のコード無しでインスタンスを簡単に生成できるようになりました。便利ですね。

参考