The Pragmatic Ball boy

iOSを中心にやってる万年球拾いの老害エンジニアメモ

MacBook Proの電源アダプタ

MBPを会社と家で使っている場合に、ただでさえMBPが重いのに電源アダプタまで持ち帰りたくないですよね。

ですが、純正の電源アダプタ(85W)を買うと8000円もするわけです・・

そこで色々探してよさげだったのがAnker PowerPort Speed PD 60です。

出力は60Wと私の使っているMBP15インチでは少々足りないですがこれを購入して使っています。

3ヶ月ほどMBP15インチ 2018のフルスペックのマシンでつかっていますが、これといった不満はありません。 当方はiOSエンジニアなのでXcodeでCPUぶん回してビルドとかしてますが、60Wでも充電されます。

値段も純正の半額以下とお手軽なのでおすすめです

SwiftUIのViewで条件によってViewを出し分ける方法

potatotips #64でLTした内容です。


SwiftUIのViewで条件によってViewをだしわけたいことがたまにあるかと思います。

SwiftUIでこのように条件によってViewを出し分けるようなコードを書いてみます。

var body: some View {
    if imageName.isEmpty {
        return Text("no image")
    } else {
        return Image(imageName)
    }
}

そうするとこのようなコンパイルエラーになってしまいます。

! Function declares an opaque return type, but the return statements in its body do not have matching underlying types

なぜかというとbodyの戻り値の型がOpaque Result Typeなので、条件によってTextを返したりImageを返すといった複数の型を返すことができないからです。

ちなみにどちらの条件でも同じ型を返す場合はOKです。

var body: some View {
    if imageName.isEmpty {
        return Text("no image")
    } else {
        return Text(imageName)
    }
}

解決方法

解決方法はざっくり3つあります

1. Group / HStack / VStack / ZStack

1つはGroupやZStackなどをつかってwrapしてやる方法

var body: some View {
    Group {
        if imageName.isEmpty {
            Text("no image")
        } else {
            Image(imageName)
        }
    }
}

2. @ViewBuilder

もう1つは@ViewBuilderアノテーションをつける方法

@ViewBuilder
var body: some View {
    if imageName.isEmpty {
        Text("no image")
    } else {
        Image(imageName)
    }
}

3. ViewBuilder.buildEither()

最後はViewBuilder.buildEither()を使う方法です。(こちらの場合は3項演算子にする必要があります)

var body: some View {
    imageName.isEmpty
        ? ViewBuilder.buildEither(first: Text("no image"))
        : ViewBuilder.buildEither(second: Image(imageName))
}

※この3つの中でどれが一番よいかは最後に紹介します

以上3つの書き方を紹介しましたが、どれもViewBuilderの力によるものです。

なぜViewBuilderを使うと条件分岐が使えるのでしょうか?

ViewBuilderを使うと条件分岐が使える理由

ViewBuilderの仕組みを説明すると ViewBuilderはfunction builderを用いて実装されています。

このfunction builderはコンパイル時に構文解析をしてif文のchainだったらbuildEitherというfunctionを使うというふうになっています。

つまりどういうことかというと

こういうコードをコンパイルすると

@ViewBuilder
var body: some View {
    if imageName.isEmpty {
        Text("Hello")
    } else {
        Image(imageName)
    }
}

このような感じにコンパイル時に解釈されるということです。

var body: some View {
    imageName.isEmpty
        ? ViewBuilder.buildEither(first: Text("no image"))
        : ViewBuilder.buildEither(second: Image(imageName))
}

ここでViewBuilderのbuildEitherの定義がどうなっているか確認してみます

extension ViewBuilder {
    public static func buildEither<TrueContent, FalseContent>(first: TrueContent) -> _ConditionalContent<TrueContent, FalseContent> where TrueContent : View, FalseContent : View

    public static func buildEither<TrueContent, FalseContent>(second: FalseContent) -> _ConditionalContent<TrueContent, FalseContent> where TrueContent : View, FalseContent : View
}

このように_ConditionalContent<TrueContent, FalseContent> を返すようになっていました。

確認のためにこのようなコードを書いて実行してみると

@ViewBuilder
var body: some View {
    if imageName.isEmpty {
        Text("Hello")
    } else {
        Image(imageName)
    }
}

戻り値の型は_ConditionalContent<Text, Image> になっていました。

このようにViewBuilderが条件分岐を_ConditionalContent<,>に変換してくれていたので、bodyの戻り値がopaque result typeであってもコンパイルが通るようになっていたわけです。

現時点で解決できない条件分岐

しかし、現時点(2019年8月)ではViewBuilderでは解決できない問題があります。

このようにswitch文やif let, if caseのようなif else以外の条件分岐を使うとコンパイルに通りません。(ちなみに@ViewBuilderをつかうとコンパイルは通りますがちゃんと動きません)

var body: some View {
    Group {
        switch foo {
        case a:
            Text("a")
        case b:
            Text("b")
        case c:
            Text("c")
        }
    }
}

これを解決するには面倒ですがif elseに書き換えましょう

var body: some View {
    Group {
        if foo == .a {
            Text("a")
        } else if foo == .b {
            Text("b")
        } else if foo == .c {
            Text("c")
        }
    }
}

ですが、どうしてもif else 以外で実装したい場合はAnyViewで、返すViewをwrapしてやると動かすことができます。

var body: some View {
    switch foo {
    case .a:
        return AnyView(Text("a"))
    case .b:
        return AnyView(Text("b"))
    case .c:
        return AnyView(Text("c"))
    }
}

まとめ

ViewBuilderのおかげでView内で条件分岐が使えるということがわかりました。

ただし、現在はif elseのみ対応していて、それ以外のswitch文などは使えません。

しかし、最悪AnyViewを使って型を消してやればif else以外の条件もつかうことができます。

最後に、条件分岐を使う場合は紹介した3つのやり方の中だとGroupやZStackなどでwrapする方法が一番オススメです。理由は@ViewBuilderを使うと利用できないswitch文などを使ってもコンパイルに通ってしまうので間違いに気づきにくいためです。buildEitherは直接使う用途ではないと思うのであえて使わなくて良いかなと思います。

アプリのDark ModeをOFFにする

既存のアプリでいきなりダークモードに対応するのは大変です。 しかしXcode11でビルドするとデフォルトでダークモードが適用されてしまいます。

もちろんダークモードをOFFにすることは可能となっています。

アプリ全体でダークモードをOFFにする方法

Info.plistのUIUserInterfaceStyleLight にしてやります。

 <key>UIUserInterfaceStyle</key>
    <string>Light</string>

UIで設定したい場合はこうします

f:id:yanamura:20190814181522p:plain

デバッグ

ダークモードのデバックはXcodeで簡単に切り替えて確認できるようになっています。

デバッグツールバーにEnvironment Overridesというボタン(トグルスイッチみたいなボタン)が新たに追加されていてこれを使います

f:id:yanamura:20190814182229p:plain

このボタンを押すとこのような画面が表示されます

f:id:yanamura:20190814182458p:plain

Interface StyleのところのトグルスイッチをONにして Dark のほうを選択するとダークモードになります。

f:id:yanamura:20190814182619p:plain

Nimbleでtupleの比較

Nimbleでtupleをequal()で比較してもコンパイルに通りません。

let tuple = (1, 2)
expect(tuple).to(equal((1, 2)))

原因

これはtupleがEquatableではないからです

対策

これはどうしようもないので以下のようにequalを使わずに回避するしかなさそうです

expect(tuple == (1, 2)).to(beTrue())

Add ability to compare tuples · Issue #227 · Quick/Nimble · GitHub

ERROR ITMS-90784

2019/6/28ころから急にAppStoreConnectにipaをアップロードするときに以下のエラーがでるようになった

ERROR ITMS-90784: "Missing bundle name. The Info.plist key CFBundleName is missing or has an empty value in the bundle with bundle identifier ''

読んでそのとおり、info.plistのBundle name(CFBundleName)が未設定だったり空の文字列だった場合に発生するので、info.plistのBundle name(CFBundleName)を設定すればエラーは解消します。

flutter doctorのエラーの解決方法 Verify that all connected devices have been paired with this computer in Xcode

このようなエラーがでてbrew xxxをしても解決しない場合

[!] iOS toolchain - develop for iOS devices (Xcode 10.2.1)
    ✗ Verify that all connected devices have been paired with this computer in Xcode.
      If all devices have been paired, libimobiledevice and ideviceinstaller may require updating.
      To update with Brew, run:
        brew update
        brew uninstall --ignore-dependencies libimobiledevice
        brew uninstall --ignore-dependencies usbmuxd
        brew install --HEAD usbmuxd
        brew unlink usbmuxd
        brew link usbmuxd
        brew install --HEAD libimobiledevice
        brew install ideviceinstaller

MaciPhoneを接続してから、flutter doctorを実行すると消えました

@propertyDelegateと@propertyWrapperの違い

2019/06/25 現在の情報

Swift EvolutionのProperty Wrappersを見ると@propertyWrapper ですが、

AppleのドキュメントのStateを見ると @propertyDelegateになっています。

f:id:yanamura:20190625135622p:plain
state

原因

仕様策定段階で名前が変わった

詳細

ここに書いてあるように最初はproperty delegatesだったのがproperty wrappersに名前が変わったそうです。

The name of the feature has been changed from "property delegates" to "property wrappers" to better communicate how they work and avoid the existing uses of the term "delegate" in the Apple developer community

AppleWWDCに間に合わせるために未fixの段階で実装していれこんじゃったために古い名前になっているようです。

Renamed the value property to wrappedValue to avoid conflicts.

こちらのvalueがwrappedValueに名前が変わる変更もbeta2の段階では反映されておらず、valueにしないとコンパイルに通りません。

6.5インチ用のスクリーンショットの必須化

2019年3月27日からは6.5インチ用のスクリーンショットも必須になったようで、AppStoreConnectで申請時に設定しないと審査に出すときにエラーになるようになっていました。

iOS 12.2 SDKを含むXcode 10.2にアップデートして、Appをビルドしてください。2019年3月27日以降、App Storeに提出されるすべてのiOS Appは、iOS 12.1以降のSDKでビルドされ、iPhone XS Maxまたは12.9インチiPad Pro(第3世代)のオールスクリーンのデザインをサポートする必要があります。

App StoreにiOS Appを提出する - Apple Developer

6.5インチのスクリーンショットは以下のサイズになります。

1242 x 2688 ピクセル(縦向き) 2688 x 1242 ピクセル(横向き)

https://help.apple.com/app-store-connect/?lang=ja-jp#/devd274dd925

DispatchQueue.main.asyncAfter(deadline: .now() + 1.0)はなぜnanosecondではなくsecondになるか

あんまり気にしてる人はいないかもしれないですが、普段何も考えずに DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) を使っているとdeadline: DispatchTime はsecond単位っぽく感じるかもしれないですが、nanosecondです。

DispatchTime

DispatchTime represents a point in time relative to the default clock with nanosecond precision.

なんで1.0足してるのに秒になるかというと演算子オーバーロードされてるんですね・・

public func + (time: DispatchTime, seconds: Double) -> DispatchTime
public func - (time: DispatchTime, seconds: Double) -> DispatchTime

理解して使ってればいいんですが、個人的にはちょっと紛らわしいと思うので DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) のほうがいいかなと思ったりします。

2018振り返り

2018年の振り返りです。

仕事

5月からコネヒトに出向することとなり、またiOSエンジニアのカムバックしママリのiOSアプリを開発しています

アウトプット

今年は去年よりは少し子育てに余裕ができたのでアウトプットを少しすることができました。

gitのhookを楽にするhuskyのSwift版を作って公開したり、

GitHub - yanamura/Captain: makes easy to manage git hooks for written in Swift products

コネヒトマルシェというコネヒトでやっている勉強会でLTしたり、

コネヒトのアドベントカレンダーを書いたりしました。

tech.connehito.com

しょぼい変更ですが、久々にOSSにコントリビュートしました

今更ですがQiitaでも書き始めてみました

yanamura - Qiita

インプット

割と今年は読みたいと思った本が多く、どれも良書でした。

ディープラーニングはやろうと思いつつ全く手付かずで意味もわかってなかったのを2冊読んで少し理解が進みました。

寄る年波に負けないように体のマネジメントも今年から頑張り始めました

プライベート

子供が二歳になりました。 相変わらず子育てが最優先で対応中です。

だいぶほっといても大丈夫な感じにはなってきましたが、最近は添い寝しないと寝ないマンになってしまい、添い寝すると自分が寝てしまって自由時間が・・となってしまうのが悩みです。。

まとめ

ここ2年は初めての子育てということでどれくらい時間がさけるか未知数すぎて 目標すらたてれなかったんですが、今年からはなにかしら目標をたててそれに向けて頑張りたいです!

gitでHEADの代わりに@を使う

いままで@派をあまり見かけたことがないので、もしかして認知度が低いのかな・・と思い今更感がすごいことを書いておきます。

ドキュメントに書いてあるように

HEADって打たなくても@(アットマーク)で代用できます。

@ alone is a shortcut for HEAD.

Git - gitrevisions Documentation

HEADでもHを入力してタブ押せば補完されますけど@のほうが1タップ少なく済むので便利です。

ただ記事とかに書くには@だとググれないのでHEADを使うべきですね・・

SwiftのDictionaryのsubscriptとupdateValueの違い

SwiftでDictionaryの値を追加、変更するのに2通りのやりかたがあります。

ひとつはsubscriptdict["key"] = "value"

もうひとつはupdateValuedict.updateValue("value", forKey: "key")

です。

どちらを使ってもDictionaryの値を変更できますが、微妙に違いがあります。

1. 戻り値

updateValueの場合は戻り値で「変更前の値」を返します。 keyが存在しない場合はnilを返します。

var dict1 = ["a": "a"]

print(dict1.updateValue("A", forKey: "a"))
print(dict1.updateValue("b", forKey: "b"))

これを実行すると

"a"
nil

となります。

2. nilをセットしたときの挙動の違い

  • subscriptの場合

    • nilをセットするとそのkey-valueがremoveされます
  • updateValueの場合は

var dict1 = ["a": "a"]

dict1["a"] = nil
print(dict1)

これを実行すると

[:]

となり、key-valueが削除されます。

var dict1 = ["a": "a"]

dict1.updateValue(nil, forKey: "a") // error: nil is not compatible with expected argument type 'String'

これはコンパイルエラーになります

var dict1: [String: String?] = ["a": "a", "b": "b"]

dict1.updateValue(nil, forKey: "b")
print(dict1)

これを実行すると

["a": Optional("a"), "b": nil]

valuenilが設定されます。

Xcode10で地味にInterfaceBuilderで変わっているところ

UIの追加

Xcode9までは右下に出ていたのが、Xcode10だと右上の方にあるボタン(○の中に□のボタン)を押すと出てくるようになりました。

  • Xcode9 f:id:yanamura:20181010164750p:plain

  • Xcode10 f:id:yanamura:20181010164843p:plain

Image Literal

Xcode9だと画像名を入力すると補完されていましたが、Xcode10だとやり方が変わりました

  • Xcode10 imageと入力すると補完でimage literalがでてくるので選択 f:id:yanamura:20181011150348p:plain

表示されたアイコンをダブルクリック f:id:yanamura:20181011150409p:plain

または、

Shift + ⌘ + Mを押すとMediaの検索が出てくるのでそこからドラック&ドロップすることでも対応できます

git2.19以降のbranchのsort

gitの2.19がリリースされました

Highlights from Git 2.19 | The GitHub Blog

個人的な目玉機能はbranchのsortが楽になったという点です。

git branch --sort=-authordate

とすると更新日順にbranchが表示されます。

更に

git config --global branch.sort -authordate

というふうにconfigに設定しておくと毎回オプションをつけなくてもgit branchするだけで更新日順に表示されます。

authordate以外には以下のようなsort optionが用意されています。

オプション 説明
--sort=numparent shows merges by how awesome they are
--sort=refname sorts branches alphabetically by their name (this is the default, but may be useful to override in your configuration)
--sort=upstream sorts branches by the remote from which they originate

Horizontal StackViewで左寄せにする

横方向のStackViewを普通に使うと余白は詰められて横幅いっぱい使うようになってしまいますが、たまに左寄せにしたい(右に余白を開けたい)場合があります。

そんなときはこのように空のViewを一番うしろに突っ込んでやることで解決できます。

let spacerView = UIView()
spacerView.setContentHuggingPriority(.defaultHigh, for: .horizontal)

stackView.addArrangedSubview(spacerView())

ただしこのやり方だとspacingを設定している場合は、余分にViewを一つ入れているため一番右に余計に一つspaceが入ってしまいます。