20201130のSwiftに関する記事は15件です。

ターミナルでCarthageの.sh実行のエラー:Build Failed Task failed with exit code 65

プロジェクトで使っているライブラリは、Carthageで管理しているものがあって、
ターミナルでPod Installではなく、
CarthageのコマンドやPodのコマンドが混じって書かれている。
そのファイルがxxx.shというファイル、
それをターミナルで実行した時には
Build Failed
Task failed with exit code 65
...
This is usually indicates that project itself failed to compile.
please check the xcodebuild log for more details: /var/folders/...carthage-xcodebuild.sHeTHo.log

と出ました。冷静に分析しましょう。
言われた「 /var/folders/...carthage-xcodebuild.sHeTHo.log」へ入ってみると、
/bin/sh -c /Users/name/Library/Caches/org.carthage.carthageKit/Derived/...Script-xxxxx.sh

cp /Users/name/ExternalModules/... /users/name/...
cp: /Users/name/ExternalModules/...is a directory(not copied).
Command PhaseScriptExectution failed with a nonzero exit code
...
The following build commands failed:
PhaseScriptExecution Run Script Script-xxxxx.sh
(1 failure)

言われた「Script-xxxxx.sh」へ入ってみると
確かに「/Users/name/ExternalModules/...」の中身をあるpathへCopyすると書かれている

なんでこのコマンドが失敗だろうと思いながら、
Copy元の「/Users/name/ExternalModules/...」へ入ってみると、空っぽです。。。
だからCopy失敗しました。

前にMacの容量が足りなかった時にいっぱいCacheを削除してしまったせいかもしれない、
「/Users/name/ExternalModules/...」の中身を復元してみると、
無事に実行できました。

●勉強になったこと:
エラーlogを分析すること。Shellのコマンドをもっと勉強すること。
Shellで書かれていることが頻繁に出てくるので、Shellのコマンドが分からないなら、logを見てもしょうがいない。
今回は他人の知恵をお借りして、shellのコマンドをわかったら、logを見てどこに失敗していたことがわかって、
詳しく原因調査ができたことが解決のカギとなりました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UINavigationControllerの戻るイベントを取得したい!

Xcode-12.0 iOS-14.0

はじめに

UINavigationController の左上に表示される「戻る」ボタンのイベントを取得したいことがたまにあります!

「戻る」ボタンのイベントを取得するために試行錯誤した結果です。

結論から言うとちゃんとイベントを取得したいなら navigationItem.leftBarButtonItem をカスタムしましょう:frowning2:

サンプル

サンプルとして FirstiViewController(赤) -> SecondViewController(青) -> ThirdViewController(緑) と push で遷移する画面構成で SecondViewController の「戻る」について考えます。

screens

SecondViewController からは Full Screen の modal 遷移(黄)もつけています。

deinit を使う

SecondViewController から戻る際は deinit が呼ばれるのでここで試してみます。

deinit {
    print("戻る!!!")
}

SecondViewController から FirstViewController に戻る際に呼ばれますがここだと ThirdViewController から popToRootViewController などで FirstViewController に戻った場合と区別することが困難です:no_good:

UINavigationControllerDelegate を使う

UINavigationController には UINavigationControllerDelegate がありこのデリゲートを使ってイベント取得を試みます。

SecondViewController から戻った場合は FirstiViewController に戻るのでデリゲートで表示されるのが FirstiViewController かどうかで下記のように判定します。

extension SecondViewController: UINavigationControllerDelegate {

    // ナビゲーション遷移の直前に呼ばれる
    func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
        if viewController is FirstViewController {
            print("戻る!!!")
        }
    }

    // ナビゲーションの遷移直後に呼ばれる
    func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
        if viewController is FirstViewController {
            print("戻る!!!")
        }
    }
}

willShow の方は ThirdViewController から popToRootViewController などで FirstViewController に戻った際も呼ばれてしまうので使うなら didShow の方になるでしょう。

「戻る」押下時にログ表示とかならできますがアラートを表示したいなど遷移を止めたいときなどは残念ながらこれではダメです:no_good:

(あと個人的に UINavigationControllerDelegate を各画面で使いたくない:no_good:

viewDidDisappear を使う

viewWillDisappearviewDidDisappear を使って試してみます。

こちらは willShow, didShow と同じようなタイミングで呼ばれます(SecondViewController が非表示になるタイミングです)。

気をつけないといけないのは modal 遷移の Full Screen などでも呼ばれることです。

SecondViewController から ThirdViewController に遷移する場合と modal 遷移の Full Screen などで他の画面に遷移する場合を判定しないといけないので下記のようにします。

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    if navigationController?.viewControllers.contains(self) == false {
        print("戻る")
    }
}

override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)
    if navigationController == nil {
        print("戻る")
    }
}

didShow と同じで「戻る」押下時にログ表示とかならできますがアラートを表示したいなど遷移を止めたいときなどは残念ながらこれではダメです:no_good:

leftBarButtonItem をカスタムする

上に書いたやつはすべて厳密に言うと「戻る」ボタンを押下したかどうかは取れていません:no_good:
スワイプで戻るでも呼ばれるし、別にボタンを置いて popViewController しても呼ばれてしまいます。それに遷移途中でイベントを取っているので遷移自体を止めることはできません(アラート出してキャンセルとかできない)。

結局のところ leftBarButtonItem をカスタムしてボタンの押下イベントを取るしかありません!!

override func viewDidLoad() {
    super.viewDidLoad()
    let button = UIButton(type: .system)
    button.addTarget(self, action: #selector(back(_:)), for: .touchUpInside)
    button.setTitle("Back", for: .normal)
    button.setImage(UIImage(named: "back"), for: .normal)
    button.titleLabel?.font = UIFont.systemFont(ofSize: 16)
    button.imageEdgeInsets = .init(top: 0, left: -8, bottom: 0, right: 0)
    navigationItem.leftBarButtonItem = .init(customView: button)
}

@objc private func back(_ sender: Any) {
    print("戻る!!!")
    navigationController?.popViewController(animated: true)
}

見た目も限りなく戻るボタン:sunglasses:

back_img

使った画像はこれです(てきとーに作りました)。

back

これで「戻る」ボタン押下イベントを完璧に取得できました:tada:

がしかし、 leftBarButtonItem をカスタムするとスワイプバックできないし iOS14 からできたロングタップのメニュー表示もできなくなります:expressionless:

結論

厳密に「戻る」ボタン押下イベントを取得したいなら leftBarButtonItem のカスタムかなという感じです。

(わたしの場合は戻るときにログとりたいという要件だったので viewDidDisappear を使いました。)

おわりに

「戻る」ボタン押下イベントの取得方法としては結局、 leftBarButtonItem のカスタムに落ち着きましたがそもそも戻るときにアラート出したいとかの場合は本当に push でいいのか遷移方法を見直すべきな気もします:no_good:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UITextDocumentProxyのselectedTextは3行以上かつ改行含めて65文字以上選択していると正しい選択部分を返さない

想定外でした。

挙動の概要

動作確認は第1・2世代のiPhoneSE (iOS14.2)において行っています。

タイトルの通り、UITextDocumentProxyのselectedTextは3行以上かつ65文字以上選択していると正しい選択部分を返しません。代わりに最初と最後の2行を結合したものを返してきます。そんなことある?

論より証拠なのでまずは実行結果の例です。
IMG_3811.jpg
IMG_3812.jpg
IMG_3813.jpg
IMG_3814.jpg

なぜか最後だけ文字数が32と判定されています。
それぞれの場面でtextDocumentProxy.selectedText?.debugDescriptionを出力すると以下のようになります。

Optional("aaaaaaaaaaaaaaaa")
Optional("aaaaaaaaaaaaaaaa\nbbbbbbbbbbbbbbbb")
Optional("aaaaaaaaaaaaaaaa\nbbbbbbbbbbbbbbbb\ncccccccccccccccc")
Optional("aaaaaaaaaaaaaaaa\ndddddddddddddddd")

挙動をさらに理解するために次の二例を見ましょう。

IMG_3815.jpg
IMG_3816.jpg

非常にわかりにくくて申し訳ないのですが、文字数は改行を含まない設定で算出しているため、改行を含める場合一枚目は64文字、二枚目が65文字です。ここを境に挙動が変わっていることがわかります。

同様の事象は3行でも起こります。

IMG_3817.jpg
IMG_3818.jpg
同様に改行を含めて64文字、65文字を境に挙動が変わります。

ところが、2行の場合はこの制限はありません。
IMG_3819.jpg

ハラスメントではないでしょうか。

挙動の問題点

さて、この挙動は実用上どう問題になるでしょうか。
まず、こちら側では得られた二行分のテキストが内部を省略されてしまったものなのか、それともそうでないのか、という判断ができません。従って次のように判断する必要があります。

  • 三行以上のテキストがselectedTextに含まれていた場合→問題ない
  • 二行のテキストが含まれていた場合→内部の省略かどうか判断ができない
  • 一行のテキストが含まれていた場合→問題ない

ちょうど二行のテキストが得られた場合のみ挙動を変えなければならないということです。
しかしユーザ側から見れば、複数行選択した場合に望む挙動を得られる場合と得られない場合が生じることになります。この奇天烈な動作は不具合にしか見えませんし、「3行以上かつ改行含めて65文字以上選択しないで下さい」とはとても頼めません。

従って実際は次のようにせざるを得ません。

❌ 三行以上のテキストがselectedTextに含まれていた場合→諦める
❌ 二行のテキストが含まれていた場合→諦める
⭕️ 一行のテキストが含まれていた場合→用いる

提供できる機能がぐっと制限されますが、やむを得ないでしょう。
恣意的にやらなければ起こらなそうな動作なのに、ドキュメントに書いていないのが本当に意味不明でした。

※こちらの環境によるものの可能性があるので、ぜひ検証をお願いします。

参照

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Combining Operatorsチートシート

Combining Operators

RxSwiftでコードを書く必要があり、学習中です。
参考文献を元に学んだ内容をすぐ振り返れるようにまとめていきます。
今回はCombining Operatorsについてまとめました。
完全に自分の解釈でコメントを載せていますが、解釈に誤りがありましたら、コメント欄より教えてください。

startWith()

example(of: "startWith") {
  // 1
  let numbers = Observable.of(2, 3, 4)

  // 2
  let observable = numbers.startWith(1)
  _ = observable.subscribe(onNext: { value in
    print(value)
  })
}

コンパイル結果

Screen Shot 2020-11-30 at 10.42.56 PM.png

Observable.concat

同じ型同士の時のみ、Concatできる!

example(of: "Observable.concat") {
  // 1
  let first = Observable.of(1, 2, 3)
  let second = Observable.of(4, 5, 6)

  // 2
  let observable = Observable.concat([first, second])

  observable.subscribe(onNext: { value in
    print(value)
  })
}

コンパイル結果

Screen Shot 2020-11-30 at 10.42.47 PM.png

concat

example(of: "concat") {
  let germanCities = Observable.of("Berlin", "Münich", "Frankfurt")
  let spanishCities = Observable.of("Madrid", "Barcelona", "Valencia")

  let observable = germanCities.concat(spanishCities)
  _ = observable.subscribe(onNext: { value in
    print(value)
  })
}

コンパイル結果

Screen Shot 2020-11-30 at 10.42.05 PM.png

concatMap

example(of: "concatMap") {
  // 1
  let sequences = [
    "German cities": Observable.of("Berlin", "Münich", "Frankfurt"),
    "Spanish cities": Observable.of("Madrid", "Barcelona", "Valencia")
  ]

  // 2
  let observable = Observable.of("German cities", "Spanish cities")
    .concatMap { country in sequences[country] ?? .empty() }

  // 3
  _ = observable.subscribe(onNext: { string in
      print(string)
    })
}

コンパイル結果

Screen Shot 2020-11-30 at 10.41.49 PM.png

mege

maxConcurrentを使うことで、同時にObserveするSequenceの上限を設定できる。

example(of: "merge") {
  // 1
  let left = PublishSubject<String>()
  let right = PublishSubject<String>()

    // 2
    let source = Observable.of(left.asObservable(), right.asObservable())

    // 3
    let observable = source.merge(maxConcurrent: 2)
    _ = observable.subscribe(onNext: { value in
      print(value)
    })

    // 4
    var leftValues = ["Berlin", "Munich", "Frankfurt"]
    var rightValues = ["Madrid", "Barcelona", "Valencia"]

    repeat {
        switch Bool.random() {
        case true where !leftValues.isEmpty:
            left.onNext("Left:  " + leftValues.removeFirst())
        case false where !rightValues.isEmpty:
            right.onNext("Right: " + rightValues.removeFirst())
        default:
            break
        }
    } while !leftValues.isEmpty || !rightValues.isEmpty

    // 5
    left.onCompleted()
    right.onCompleted()
  }

コンパイル結果

Screen Shot 2020-11-30 at 10.41.22 PM.png

combineLatest

全てのObservableから値が発信されるまで、何もしない。異なる方のSequenceをまとめて、最新の値を取り出す。

example(of: "combineLatest") {
  let left = PublishSubject<String>()
  let right = PublishSubject<String>()

    // 1
//    let observable = Observable.combineLatest(left, right) {
//      lastLeft, lastRight in
//      "\(lastLeft) \(lastRight)"
//    }

// 1   combineLatestでタプルにして、filterをかける方法がよく使われる。
//    let observable = Observable
//      .combineLatest(left, right) { ($0, $1) }
//      .filter { !$0.0.isEmpty }


    // 1: Stringが確か受付内ため、余り使われない方法
    let observable = Observable.combineLatest([left, right]) {
      strings in strings.joined(separator: " ")
    }


    _ = observable.subscribe(onNext: { value in
      print(value)
    })

    // 2
    print("> Sending a value to Left")
    left.onNext("Hello,")
    print("> Sending a value to Right")
    right.onNext("world")
    print("> Sending another value to Right")
    right.onNext("Swift")
    print("> Sending another value to Left")
    left.onNext("Have a good day,")

    left.onCompleted()
    right.onCompleted()
  }


コンパイル結果

Screen Shot 2020-11-30 at 10.41.09 PM.png

combine user choice and value

ユーザーの設定が変更した時に、表示値を変える

example(of: "combine user choice and value") {
  let choice: Observable<DateFormatter.Style> = Observable.of(.short, .long)
  let dates = Observable.of(Date())

  let observable = Observable.combineLatest(choice, dates) {
    format, when -> String in
    let formatter = DateFormatter()
    formatter.dateStyle = format
    return formatter.string(from: when)
  }

  _ = observable.subscribe(onNext: { value in
    print(value)
  })
}

コンパイル結果

Screen Shot 2020-11-30 at 10.40.52 PM.png

zip

indexed sequencing: ペアが成立しなくなったら、zipは値を発しなくなるが、observableがCompleteするまで処理が終わらないため、observableが完了していることを確認すること。

example(of: "zip") {
  enum Weather {
    case cloudy
    case sunny
  }

    let left: Observable<Weather> = Observable.of(.sunny, .cloudy, .cloudy, .sunny, .cloudy)
  let right = Observable.of("Lisbon", "Copenhagen", "London", "Madrid", "Vienna")

    let observable = Observable.zip(left, right) { weather, city in
      return "It's \(weather) in \(city)"
    }
    _ = observable.subscribe(onNext: { value in
      print(value)
    })

  }

コンパイル結果

Screen Shot 2020-11-30 at 10.40.38 PM.png

withLatestFrom

example(of: "withLatestFrom") {
  // 1
  let button = PublishSubject<Void>()
  let textField = PublishSubject<String>()

  // 2
//the trigger observableが撮りガー
   let  observable  =  textField.sample(button)
//data Observableがトリガー
//  let observable = button.withLatestFrom(textField)
  _ = observable.subscribe(onNext: { value in
    print(value)
  })

  // 3

  textField.onNext("Par")
  textField.onNext("Pari")
  textField.onNext("Paris")
  button.onNext(())
  button.onNext(())
}

コンパイル結果

Screen Shot 2020-11-30 at 10.40.24 PM.png

amb

二つのObservableのどちらをつかうかAmbiguousな場合。 使い所:connecting to redundant servers and sticking with the one that responds first.

example(of: "amb") {
  let left = PublishSubject<String>()
  let right = PublishSubject<String>()

  // 1
  let observable = left.amb(right)
  _ = observable.subscribe(onNext: { value in
    print(value)
  })

  // 2
  left.onNext("Lisbon")
  right.onNext("Copenhagen")
  left.onNext("London")
  left.onNext("Madrid")
  right.onNext("Vienna")

  left.onCompleted()
  right.onCompleted()
}

コンパイル結果

Screen Shot 2020-11-30 at 10.40.08 PM.png

switchLatest

example(of: "switchLatest") {
  // 1
  let one = PublishSubject<String>()
  let two = PublishSubject<String>()
  let three = PublishSubject<String>()

  let source = PublishSubject<Observable<String>>()

    // 2
    let observable = source.switchLatest()
    let disposable = observable.subscribe(onNext: { value in
      print(value)
    })

    // 3
    source.onNext(one)
    one.onNext("Some text from sequence one")
    two.onNext("Some text from sequence two")

    source.onNext(two)
    two.onNext("More text from sequence two")
    one.onNext("and also from sequence one")

    source.onNext(three)
    two.onNext("Why don't you see me?")
    one.onNext("I'm alone, help me")
    three.onNext("Hey it's three. I win.")

    source.onNext(one)
    one.onNext("Nope. It's me, one!")
    disposable.dispose()
  }

コンパイル結果

Screen Shot 2020-11-30 at 10.39.53 PM.png

reduce

注意:sequencesが完了しないと、Summaryがemitされない。

example(of: "reduce") {
  let source = Observable.of(1, 3, 5, 7, 9)

  // 1
//  let observable = source.reduce(0, accumulator: +)
    let observable = source.reduce(0){summary, newValue in
        return summary + newValue
    }

  _ = observable.subscribe(onNext: { value in
    print(value)
  })
}

コンパイル結果

Screen Shot 2020-11-30 at 10.39.33 PM.png

scan

累計、統計、状態をコンピュートする際に使える。

値が変わると毎回、Observableから値がEmitされる。

example(of: "scan") {
  let source = Observable.of(1, 3, 5, 7, 9)

  let observable = source.scan(0, accumulator: +)
  _ = observable.subscribe(onNext: { value in
    print(value)
  })
}

コンパイル結果

Screen Shot 2020-11-30 at 10.38.40 PM.png

練習

example(of: "Challeng1") {
  let source = Observable.of(1, 3, 5, 7, 9)

    let scanedObservable = source.scan(0, accumulator: +)
    let observable =  Observable.zip(source, scanedObservable){current, total -> String in
        return "Total:\(String(total)), Current:\(String(current)))"
    }

  _ = observable.subscribe(onNext: { value in
    print(value)
  })
}

コンパイル結果

Screen Shot 2020-11-30 at 10.38.28 PM.png

練習

acc(current, accumuratedValue)となるから、acc.1 + currentとしている。

//acc(current, accumuratedValue)となるから、acc.1 + currentとしている。
example(of: "Challeng2") {
  let source = Observable.of(1, 3, 5, 7, 9)

    let  observable = source.scan((0, 0)) { acc, current in
        print("acc\(acc)")
        print("current\(current)")
        return (current, acc.1 + current)
    }

    _ = observable.subscribe(onNext: { tuple in
        print("Value = \(tuple.0)   Running total = \(tuple.1)")
    })

}

コンパイル結果

Screen Shot 2020-11-30 at 10.38.17 PM.png

参考文献

1.Combining Operators Written by Florent Pillet
Chapter9 Transforming Operators Written by Scott Gardner
詳細が気になる方はこちらをご参照ください!
https://www.raywenderlich.com/13285844-rxswift-reactive-programming-with-swift-update-now-available

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【備忘録】Swift 実践入門について 第5章

Swift 実践入門のまとめ。
分からない部分の抜粋も記載し、解決できたら随時更新していきます。
なお、ここに記載している以外でも「わけわからん…」となっている部分も多々ありますが、
今は必要ない、と言い聞かせて飛ばしています。

第5章
制御構文 上から下へ流れていくプログラムの動きを変更したりすること。
理解度:60%くらいか

5.2
条件分岐
・if-let文
let a = Optional(1)
if let b = a {
print(“値は\(a)です”)
} else {
print(“値が存在しません”)
}
値は1です

・guard文
func printIfPositive() {
let a = 1
guard a > 0 else {
print(“aは0未満です”) //falseならこの部分が実行される
return //スコープ外に退出
}
print(a)
}
printIfPositive() //結果は1

・switch文…Bool型ではなく、caseを設定していくことでもっと細かい条件分岐が可能。defaultをつけることで条件に網羅性が出るが、細かく設定した方が追加で条件を設定する場合に強い。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Cocoa][Swift]iOS 14 でのトラッキングのオプトインについて

広告を使って Android と iOS のモバイルアプリを宣伝する場合、広告のクリックがアプリのインストールとアプリ内操作の獲得にどの程度貢献しているかを把握することが重要だ。

iOS の IDFA(広告ID)は、この情報を計測するために利用されている。

iOS14のトラッキングは、以下の種類がある。

  • SKAdNetwork (SKA)
    • ユーザの個人情報を利用しない。
    • アプリのインストールと特定のキャンペーンを関連付けるためのもの。
  • AppTrackingTransparency (ATT) フレームワーク
    • IDFAを扱うためのもの。

iOS14から、ASIdentifierManager の isAdvertisingTrackingEnabled は非推奨。

ASIdentifierManager *identifierManager = [ASIdentifierManager sharedManager];
if ([identifierManager isAdvertisingTrackingEnabled]) {
    NSLog(@"[idfa] isAdvertisingTrackingEnabled enabled");
} else {
    NSLog(@"[idfa] isAdvertisingTrackingEnabled disabled");
}
NSString *idfa = identifierManager.advertisingIdentifier.UUIDString;
NSLog(@"[idfa] %@", idfa);

コードの互換性は保たれていて、iOS14では isAdvertisingTrackingEnabled はfalseを返す。

iOS14では、設定のプライバシー > 広告 画面がなくなった。

広告

システムが表示するダイアログの文言はInfo.plistで設定する。

ダイアログ

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>NSUserTrackingUsageDescription</key>
    <string>広告トラッキングしますよ。</string>
</dict>
</plist>

ユーザに許可を得るコードは以下のとおり。

 if (@available(iOS 14, *)) {
    [ATTrackingManager requestTrackingAuthorizationWithCompletionHandler:
        ^(ATTrackingManagerAuthorizationStatus status) {
        NSLog(@"in requestTrackingAuthorizationWithCompletionHandler");
        if (status == ATTrackingManagerAuthorizationStatusAuthorized) {
            NSLog(@"[requestTrackingAuthorization] authorized");
            NSString *idfa = identifierManager.advertisingIdentifier.UUIDString;
            NSLog(@"[idfa] %@", idfa);
        } else if (status == ATTrackingManagerAuthorizationStatusDenied) {
            NSLog(@"[requestTrackingAuthorization] denied");
        } else if (status == ATTrackingManagerAuthorizationStatusRestricted) {
            NSLog(@"[requestTrackingAuthorization] restricted");
        } else {
            NSLog(@"[requestTrackingAuthorization] notDetermined");
        }
   }];
} else {
    // Fallback on earlier versions
}

確認ダイアログは一度しか表示しないが、設定は後から有効にできる。その画面を表示するコードは以下のとおり。

 NSURL *url = [NSURL URLWithString:UIApplicationOpenSettingsURLString];
[[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];

アプリ設定
設定

【関連情報】
Cocoa Advent Calendar 2020

Cocoa.swift

Cocoa勉強会 関東

Cocoa練習帳

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Cocoa][Swift]Cocoa Update

以前のWWDCでは、Cocoaの変更内容を『Cocoa Update』という名称で発表していた。近年では『What's New in Cocoa』という名称で発表されていたが、今年のWWDCのセッションにはCocoaという用語が使われているものがない。Apple Developerサイトを確認しても、Cocoaという用語を見つけることが困難だ。

理由は色々あるだろう。以前はAppleのフレームワークの概念の理解させていということでCocoaという名前で説明していたが、もう、Cocoaという名称で設計思想を啓蒙する必要がなくなったということだろうか。

Cocoaと呼ばれていたフレームワークの基本的な考え方をApple Developerサイトから入手できた『Objective-Cプログラミングの概念(CocoaEncyclopedia.pdf)』の目次から拾ってみる。

  • クラスクラスタ(Class Cluster)
  • デリゲートとデータソース
  • インストロスペクション
    実行時にオブジェクト自身の内部の詳細を調べる機能
  • オブジェクトの初期化
  • Model-View-Controllerパターン
  • オブジェクトモデリング
  • オブジェクトの可変性
  • Receptionistパターン
    キー値監視やKVO通知など
  • ターゲット - アクション機構
  • フレームワークの相互乗り入れについて
    Core FoundationとFoundationとで互換性のあるデータ型

WWDC20の『Platforms State of the Union』から、フレームワークの更新に相当するものを抜き出してみる。

  • Apple Silicon
    たとえば、SDKを提供してる場合はライブラリはXCFrameworkでないと困るはず。
  • Mac Catalystの進化
    iOSアプリケーションのmacOS対応の方法の選択肢が増えた。
  • macOS Big Sur
    Aquaではなくなった。
  • Widget
  • App Clips
  • SwiftとSwiftUI
    Cocoaとは異なる概念のものも。

もはや、Cocoaという用語が通じない世代の開発者も存在していると思うので、世代間ギャップを感じる。

【関連情報】
Cocoa Advent Calendar 2020

Cocoa.swift

Cocoa勉強会 関東

Cocoa練習帳

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Xcodeのビルド時にアーキテクチャが異なるframeworkファイルを差し替える

qnote Advent Calendar 2020 の2日目です。

とあるプロジェクトで使用している外部ライブラリが実機用とシミュレーター用の2つのframeworkファイルがありプロジェクトファイルには実機用がリンクされています。
実機用のファイルには実機のアーキテクチャが含まれているバイナリファイルがあり、シミュレータ用のファイルにはシミュレータのアーキテクチャがあり、実機のアーキテクチャにはシミュレータが含まれていないためシミュレーターで実行ができない状態になっているのでシミュレーターで実行ができるようにしたいと思います。
シミュレータで実行する際にビルド時にエラーになってしまうのでビルド前差し替えてビルド後に戻せば良いのではと思いましたのでこの方法で進んでみます。

frameworkのバイナリに含まれているアーキテクチャについて

詳しくは以下のサイトをご確認ください。
https://qiita.com/fuwamaki/items/ce1cd438ec5dfd4ccb9a#simulator%E3%81%A7%E3%81%AF%E3%83%93%E3%83%AB%E3%83%89%E3%81%A7%E3%81%8D%E3%82%8B%E3%81%AE%E3%81%AB%E5%AE%9F%E6%A9%9F%E3%81%A0%E3%81%A8%E3%81%A7%E3%81%8D%E3%81%AA%E3%81%84
https://blog.ymyzk.com/2015/12/ios-tvos-watchos-architectures/

どのように差し替えるか

・Build PhaseのRun Script
・スキームのPre-actionsとPost-actions
のどちらかで差し替えをします。
調べると大体シェルスクリプトが出てきますがせっかくなのでSwiftでやっていこうと思います。
Run Scriptはドラッグ&ドロップで移動できるのでビルド前の方はCompile Sourcesより前に配置します。

注意点

どちらもビルドが失敗するとビルド後の処理が実行されないので万が一のことを考えてGit等で元に戻せるようにしておいた方が良いです。

プロジェクトフォルダの状態

プロジェクトを新規作成してデフォルトでできる(プロジェクト名).xcodeprojファイルがあるディレクトリに
(プロジェクト名)/SDK/hoge/lib/iphoneos/hoge.framework
(プロジェクト名)/SDK/hoge/lib/iphonesimulator/hoge.framework
に各frameworkファイル(iphoneosが実機、iphonesimulatorがシミュレータ)と
script/script.swift
にスクリプト用Swiftファイルがある状態です。

Swiftのファイルの実行

ビルド前

#ログを書き出して確認する場合は下の行を実行する
#exec > ${PROJECT_DIR}/script/prebuild.log 2>&1

/usr/bin/env xcrun --sdk macosx swift ${SRCROOT}/script/script.swift "pre" ${PROJECT_DIR} ${PLATFORM_NAME}

ビルド後

#ログを書き出して確認する場合は下の行を実行する
#exec > ${PROJECT_DIR}/script/postbuild.log 2>&1

/usr/bin/env xcrun --sdk macosx swift ${SRCROOT}/script/script.swift "post" ${PROJECT_DIR} ${PLATFORM_NAME}

preの部分がpostになっています。

script.swiftの中身は以下の内容になっています。

script.swift
#!/usr/bin/swift

import Cocoa
import Foundation

let actionType = CommandLine.arguments[1]       // 引数1 "pre" または "post"
let projectDirectory = CommandLine.arguments[2] // 引数2 ${PROJECT_DIR}
let platform = CommandLine.arguments[3]         // 引数3 ${PLATFORM_NAME}

let frameworkFileName = "hoge.framework"
let tempFrameworkFileName = "temp_\(frameworkFileName)"
let libraryDirectory = "\(projectDirectory)/(プロジェクト名)/SDK/hoge/lib/"
let iphoneFrameworkDirectory = "\(libraryDirectory)/iphoneos/"
let iphoneOriginalPath = "\(iphoneFrameworkDirectory)\(frameworkFileName)"
let iphoneTempPath = "\(iphoneFrameworkDirectory)\(tempFrameworkFileName)"
let simulatorFrameworkDirectory = "\(libraryDirectory)/iphonesimulator/"
let simulatorOriginalPath = "\(simulatorFrameworkDirectory)\(frameworkFileName)"

class Script {

    var isSimulator: Bool {
        return platform == "iphonesimulator"
    }

    func preAction() {
        reverseActionIfNeeded()
        guard isSimulator else {
            return
        }
        let _ = rename(iphoneFrameworkDirectory, oldName: frameworkFileName, newName: tempFrameworkFileName)
        let _ = copy(simulatorOriginalPath, toPathName: iphoneOriginalPath)
    }

    func postAction() {
        guard isSimulator else {
            return
        }
        let _ = remove(iphoneOriginalPath)
        let _ = rename(iphoneFrameworkDirectory, oldName: tempFrameworkFileName, newName: frameworkFileName)
    }

    func reverseActionIfNeeded() {
        guard FileManager.default.fileExists(atPath: iphoneTempPath) else {
            return
        }
        let _ = remove(iphoneOriginalPath)
        let _ = rename(iphoneFrameworkDirectory, oldName: tempFrameworkFileName, newName: frameworkFileName)
    }

    func copy(_ atPathName: String, toPathName: String) -> Bool {
        do {
            try FileManager.default.copyItem(atPath: atPathName, toPath: toPathName)
        } catch {
            print(error)
            return false
        }
        return true
    }

    func rename(_ pathName: String, oldName: String, newName: String) -> Bool {
        let atPathName = "\(pathName)/\(oldName)"
        let toPathName = "\(pathName)/\(newName)"
        do {
            try FileManager.default.moveItem(atPath: atPathName, toPath: toPathName)
        } catch {
            return false
        }
        return true
    }

    func remove(_ pathName: String) -> Bool {
        do {
            try FileManager.default.removeItem(atPath: pathName)
        } catch {
            return false
        }
        return true
    }
}

if actionType == "pre" {
    Script().preAction()
} else if actionType == "post" {
    Script().postAction()
}

大まかに動作内容を解説します。
ビルド前のpreAction()でシミュレータの場合に
iphoneos/hoge.frameworkiphoneos/temp_hoge.frameworkにリネームして
iphonesimulator/hoge.frameworkiphoneos/hoge.frameworkにコピーします。
これでプロジェクトファイルにリンクされている実機用のhoge.frameworkをシミュレータ用のファイルと入れ替えることができ、ビルドが成功するようになります。
ビルド後のpostAction()にて
iphoneos/hoge.frameworkを削除して
iphoneos/temp_hoge.frameworkiphoneos/hoge.frameworkにリネームしています。
これで元の状態に戻ります。

注意点でも記載しましたがビルドに失敗した場合にpostAction()が実行されないので
preAction()のreverseActionIfNeeded()にてiphoneos/temp_hoge.frameworkが存在していた場合iphoneos/hoge.frameworkがシミュレータ用のままなので最初に元の状態に戻してから処理を行うようにしています。

参考
https://qiita.com/kuluna/items/2225298e8a9a7b3b3ef6

中々こういう状況にはならないかと思いますが、もし誰かが同じことで困った時の助けになればと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Swiftにおける基本的な型の紹介〜Bool型〜

Bool型とは

Bool型とは真理値のことで、ある命題が真であるか偽であるかを表す値です。
つまり、式が型推論されない場合はBool型の値であるtrueかfalseを返します。

let a = true   // Bool型
let b = false   // Bool型
let c: Bool
c = 10 == 10   // true

論理演算

論理演算とは真理値に対する演算になります。
Bool型には、否定・論理積・論理和の3つの論理演算子が用意されています。

論理演算をうまく使うことにより様々な条件式を記載することが可能です。

・ 否定

否定は、真理値を逆にする論理演算です。
Bool型の値の否定を求めるには、!演算子を利用します。

否定したい変数や定数に!をつけることにより、逆の真理値を導くことが可能になります。

let a = true   // true
let b = !a   // false

・ 論理積

論理積とは、与えられた複数の真理値全てが真であれば真となる論理演算子です。

2つ以上の真理値の論理積を求める場合には&&を使用します。
&& は、〜かつ〜 の、かつの部分だと思ってください。

true && true の場合は全ての真理値が true なので結果も true になります。
しかし、true && false の場合は全ての真理値が true ではないので結果は false になります。

国語のテストで、点数が50点を超えているかどうか調べる際に論理積を使うとこうなります。
たろう:100点、あやか:60点、まこと:10点の場合。

let tarou = 100   // たろうは100点
let ayaka = 60   // あやかは60点
let makoto = 10   // まことは10点

var result: Bool   // Bool型

//たろうの点数が50点以上 &&(かつ) あやかの点数が50点以上 -> true && true
result = tarou >= 50 && ayaka >= 50   // true
//太郎の点数が50点以上 &&(かつ) まことの点数が50点以上 -> true && false 
result = tarou >= 50 && makoto >= 50   //false
//三人のテストの結果が50点以上 -> true && true && false
result = tarou >= 50 && ayaka >= 50 && makoto >= 50   //false

少しわかりにくいですかね・・・?(笑)

とにかく論理積を使っていた場合、
一つでもfalseが存在したら結果はfalseになるということです!

・ 論理和

論理和とは、与えられた真理値の少なくともどれか一つが真であれば、結果も真となる論理演算です。

2つ以上の真理値の論理積を求める場合には||を使用します。
|| は、〜または〜 の、またはの部分だと思ってください。

true || true の場合は、どちらもtrueなので、結果はtrueになります。
true || false の場合は、片方がtrueなので、結果はtrueになります。
false || false の場合は、両方ともfalseでtrueが存在しないので、結果はfalseになります。

先ほどの例を使うとこのようになります。

let tarou = 100   // たろうは100点
let ayaka = 60   // あやかは60点
let makoto = 10   // まことは10点

var result: Bool   // Bool型

//たろうの点数が50点以上 ||(または) あやかの点数が50点以上 -> true || true
result = tarou >= 50 || ayaka >= 50   // true
//太郎の点数が50点以上 ||(または) まことの点数が50点以上 -> true || false 
result = tarou >= 50 || makoto >= 50   //true
//三人のテストの結果が50点以上 -> true || true || false
result = tarou >= 50 || ayaka >= 50 || makoto >= 50   //true

論理和の場合は、どれか一つでもtrueが入っていれば結果はtrueになるということです!

Bool型は最初のうちはよくわかりませんでしたが、
使っていく内に結構慣れていきました!

分からなくても色々試して調べて使えるようにしていきましょう!

以上、ありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOSでの半モーダル/ハーフモーダルの実装についてのまとめ

はじめに

  • iOSのUIでハーフモーダルを見かけることが増えました。Apple純正のアプリでも実装されることが多く、今後実装することが多くなると思います。 今後の実装の参考になるように情報をまとめたいと思いました。

半モーダル/ハーフモーダルとは

・モードの完全遷移が起こらない
・モードを多重化しながらパラレルに対話可能
・モードを終了させずにモードからの一時退避が可能
・擬似的なマルチウインドウ・インターフェイスに応用可能
・スワイプなどインタラクションコストの低い操作方法によってモードの切り替えが可能
  • Pull dissmissや画面遷移せずにタスクを切り替える機能などによく使われます
  • 代表的なものはAppleのMapアプリです

Kapture 2020-11-30 at 16.59.16.gif

 実装方法

UIModalPresentationStyleを使う

Kapture 2020-11-30 at 16.57.26.gif

     let vc = UIViewController()
     vc.modalPresentationStyle = .pageSheet
     present(vc, animated: true, completion: nil)
  • この実装だと下階層の画面に対してタップできないなどの仕様の制限があります。

ライブラリを使う

  • すでに完成度の高いライブラリが存在するので実装コストを下げることができます。

SCENEE/FloatingPanel

  • Apple純正のアプリのような動きを再現できます

Kapture 2020-11-30 at 18.10.27.gif

Kapture 2020-11-30 at 18.12.03.gif

slackhq/PanModal

Kapture 2020-11-30 at 18.16.30.gif

自前で実装する

  • より完成度の高い完成度を目指す場合は自前で実装することも選択肢に入ります。実装する上でのポイントをあげておきます。

徹底調査

  • 完成度の高いライブラリが多いので中身を覗いてみると理解が進みます。とくに上記で挙げたライブラリは参考になります。
  • cocoacontrolsなどを使うとたくさんのライブラリを見つけることができます。

UIViewControllerTransitioningDelegate

  • この実装をすることで表示のアニメーションのカスタマイズと表示後の画面もカスタマイズできます
let vc = UIViewController()
vc.modalPresentationStyle = .custom
vc.transitioningDelegate = self
present(vc, animated: true, completion: nil)
extension UIViewController: UIViewControllerTransitioningDelegate {
    public func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
        return UIPresentationController(presentedViewController: presented, presenting: presenting)
    }

    public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return PresentationAnimator()
    }

    public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return DismissionAnimator()
    }
}

PSPDFTouchForwardingView

final class PSPDFTouchForwardingView: UIView {

    final var passthroughViews: [UIView] = []

    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        guard let hitView = super.hitTest(point, with: event) else { return nil }
        guard hitView == self else { return hitView }

        for passthroughView in passthroughViews {
            let point = convert(point, to: passthroughView)
            if let passthroughHitView = passthroughView.hitTest(point, with: event) {
                return passthroughHitView
            }
        }

        return self
    }
}
private var touchForwardingView: PSPDFTouchForwardingView?
override func presentationTransitionWillBegin() {
    super.presentationTransitionWillBegin()
    touchForwardingView = PSPDFTouchForwardingView(frame: containerView!.bounds)
    containerView?.insertSubview(touchForwardingView, at: 0)
}

まとめ

  • 近年ハーフモーダルのUIを採用するアプリが増えてきている
  • 完成度の高いライブラリも多く実装コストを下げることができる
  • 自前実装もポイントを押さてしまえば簡単です?‍♂️

参考リンク

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOSでの半モーダル/ハーフモーダルの実装についてまとめ

はじめに

  • iOSのUIでハーフモーダルを見かけることが増えました。Apple純正のアプリでも実装されることが多く、今後実装することが多くなると思います。 今後の実装の参考になるように情報をまとめたいと思いました。

半モーダル/ハーフモーダルとは

・モードの完全遷移が起こらない
・モードを多重化しながらパラレルに対話可能
・モードを終了させずにモードからの一時退避が可能
・擬似的なマルチウインドウ・インターフェイスに応用可能
・スワイプなどインタラクションコストの低い操作方法によってモードの切り替えが可能
  • Pull dissmissや画面遷移せずにタスクを切り替える機能などによく使われます
  • 代表的なものはAppleのMapアプリです

Kapture 2020-11-30 at 16.59.16.gif

 実装方法

UIModalPresentationStyleを使う

Kapture 2020-11-30 at 16.57.26.gif

     let vc = UIViewController()
     vc.modalPresentationStyle = .pageSheet
     present(vc, animated: true, completion: nil)
  • この実装だと下階層の画面に対してタップできないなどの仕様の制限があります。

ライブラリを使う

  • すでに完成度の高いライブラリが存在するので実装コストを下げることができます。

SCENEE/FloatingPanel

  • Apple純正のアプリのような動きを再現できます

Kapture 2020-11-30 at 18.10.27.gif

Kapture 2020-11-30 at 18.12.03.gif

slackhq/PanModal

Kapture 2020-11-30 at 18.16.30.gif

徹底調査

  • 完成度の高いライブラリが多いので中身を覗いてみると理解が進みます。とくに上記で挙げたライブラリは参考になります。
  • cocoacontrolsなどを使うとたくさんのライブラリを見つけることができます。

自前で実装する

  • より完成度の高い完成度を目指す場合は自前で実装することも選択肢に入ります。実装する上でのポイントをあげておきます。

UIViewControllerTransitioningDelegate

  • この実装をすることで表示のアニメーションのカスタマイズと表示後の画面もカスタマイズできます
let vc = UIViewController()
vc.modalPresentationStyle = .custom
vc.transitioningDelegate = self
present(vc, animated: true, completion: nil)
extension UIViewController: UIViewControllerTransitioningDelegate {
    public func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
        return UIPresentationController(presentedViewController: presented, presenting: presenting)
    }

    public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return PresentationAnimator()
    }

    public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return DismissionAnimator()
    }
}

PSPDFTouchForwardingView

final class PSPDFTouchForwardingView: UIView {

    final var passthroughViews: [UIView] = []

    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        guard let hitView = super.hitTest(point, with: event) else { return nil }
        guard hitView == self else { return hitView }

        for passthroughView in passthroughViews {
            let point = convert(point, to: passthroughView)
            if let passthroughHitView = passthroughView.hitTest(point, with: event) {
                return passthroughHitView
            }
        }

        return self
    }
}
private var touchForwardingView: PSPDFTouchForwardingView?
override func presentationTransitionWillBegin() {
    super.presentationTransitionWillBegin()
    touchForwardingView = PSPDFTouchForwardingView(frame: containerView!.bounds)
    containerView?.insertSubview(touchForwardingView, at: 0)
}

まとめ

  • 近年ハーフモーダルのUIを採用するアプリが増えてきている
  • 完成度の高いライブラリも多く実装コストを下げることができる
  • 自前実装もポイントを押さてしまえば簡単です?‍♂️

参考リンク

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【備忘録】Swift 実践入門について 第4章

Swift 実践入門のまとめ。
分からない部分の抜粋も記載し、解決できたら随時更新していきます。
なお、ここに記載している以外でも「わけわからん…」となっている部分も多々ありますが、
今は必要ない、と言い聞かせて飛ばしています。

第4章
コレクション let a = [1, 2, 3]など要素を複数持ちたいとき。
理解度:80%くらいか

4.2
Array型…[1, 2, 3]

4.3
Dictionary型…let dictionary = [“a” : 1, “b” : 2] キーワードの“Key”とValue

4.4
範囲型…let range = 1..< 4

4.5
String型のコレクション
Character型…単一文字を示す
String.Index型…startIndexやendIndexなど

4.6
シーケンス…値にアクセスしていく順番。普通は左から順番にアクセスしていくが変更も可能。

・filter…複数のエレメントを持つ変数aに対してlet b = a.filter({element in element % 2 == 0})とすれば、定数bは変数aの偶数番目のエレメントを持つようになる。

・map…全エレメントに変更を加える。let b = a.map({element in element * 2})とすれば、定数bは変数aを2倍したエレメントを持つようになる。

・flatMap…let b = a.flatMap({value in [value, value + 1]})とすれば、定数bはaのエレメントとそのエレメントに1を足した値をそれぞれ持つ。

・compactMap…mapに似ているが、変更できない値があった場合は無視してコンパクトにする。

・コレクションのプロトコル
let a = [1, 2, 3, 4, 5]
a[2] //3
a.isEmpty // false
a.count //5
a.first //1
a.last //5

「.プロパティ」にまだ慣れない。本を読んでいてもSwift側で決められたプロパティなのか、こちらが個人的に決めたものなのかが曖昧。ただ4章まで進めてきて、少しづつ理解が深まってきている気はする。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Swift] Resultで例外をいい感じに処理する

注意

これはネタエントリーです

Swiftの公式見解は「例外は例外としてちゃんと処理しろ。Resultはそういうものじゃねぇ」です。
ご理解の上お読みいただけますようお願いいたします。

なにこれ?

例外処理を do - try - catchを使わずResultで処理しちゃおうという奴です。

準備

こちらのエントリにある ifSuccess / ifFailure を利用しますので確認しておいてください。

じゃあやってみようか

特に難しいことをしようというのではありませんのでコードをドーンと出しちゃいます。

以下のようなコードをResultで書き換えちゃいます。

struct HogeError: Error {}
func hoge(_ s: String) throws -> Int {
  guard let i = Int(s) else { throw HogeError() }
  return i
}

do {
  print(try hoge("123"))
}
catch {
  print(error)
}

do {
  print(try hoge("abc"))
}
catch {
  print(error)
}

とくに解説はいりませんね。

では書き換えます。

struct HogeError: Error {}
func hoge(_ s: String) throws -> Int {
  guard let i = Int(s) else { throw HogeError() }
  return i
}

Result { try hoge("123") }
  .ifSuccess { print($0) }
  .ifFailure { print($0) }

Result { try hoge("abc") }
  .ifSuccess { print($0) }
  .ifFailure { print($0) }

僕にはかなり読みやすいのですが、皆さんはどうでしょう?

エラーを複雑にしてみる 1

例外処理では複雑な例外に対処する必要が多分に発生します。
まずはenumな例外を見てみましょう。

enum HogeError: Error {
 case notNumber
 case minus
 case lessThan10(Int)
}
func hoge(_ s: String) throws -> Int {
  switch Int(s) {
  case nil: throw HogeError.notNumber
  case (...0)?: throw HogeError.minus
  case let n? where n < 10 : throw  HogeError.lessThan10(n)
  case let n?: return n
  }
}

do-try-catchでは以下のようになります。

do {
  print(try hoge("abc"))
}
catch HogeError.notNumber {
  print("not number")
}
catch HogeError.minus {
  print("minus")
}
catch let HogeError.lessThan10(n) {
  print(n, "is less than 10.")
}
catch {
  print("we caught unknown error.", error)
}

ごく普通です。

ではこれをResultで処理してみましょう。
そのために新たなstructを導入しResultのextensionに手を加えます。
長いので読み飛ばしましょう

struct ResultErrorHandler<Failure: Error> {
  let failure: Failure?
  let handled: Bool

  func ifFailure<F: Error>(as type: F.Type, _ f: (F) -> Void) -> Self {
    if let error = failure as? F {
      f(error)

      return .init(failure: failure, handled: true)
    }

    return self
  }

  func ifFailure(_ f: (Failure) -> Void) {
    if handled { return }
    guard let failure = failure else { return }

    f(failure)
  }
}

extension Result {
  @discardableResult
  func ifSuccess(_ f: (Success) -> Void)-> Self {
    if case let .success(v) = self { f(v) }
    return self
  }

  func ifFailure(_ f: (Failure) -> Void) {
    if case let .failure(error) = self { f(error) }
  }

  func ifFailure<F: Error>(as type: F.Type, _ f: (F) -> Void) -> ResultErrorHandler<Failure> {
    guard case let .failure(error) = self else {
      return .init(failure: nil, handled: false)
    }
    if let e = error as? F {
      f(e)
      return .init(failure: error, handled: true)
    }

    return .init(failure: error, handled: false)
  }
}

ここにJump

これで準備が出来ましたのでResultを使った例外処理に書き換えてみましょう。

Result<Void, Error> { print(try hoge("abc")) }
  .ifFailure(as: HogeError.self) { error in
    switch error {
    case .notNumber: print("not number")
    case .minus: print("minus")
    case let .lessThan10(n): print(n, "is less than 10.")
    }
  }
  .ifFailure { error in
    print("we caught unknown error.", error)
  }

読み飛ばしていただいたextensionで実装されたifFailure(_:)メソッドに渡された関数/クロージャは、その前に実行されたifFailure(as:)メソッドがすでにエラーハンドリングを終えている場合は実行されません。
つまりこの場合ですとFailureがHogeErrorしか起こりえませんので、ifFailure(_:)メソッドに渡した関数/クロージャが実行されることはありません。

残念ながらenumの個別のcaseごとに処理が分けられません。
この点はdo-try-catchの方がよいかもしれません。
ただし、こちらの場合はswitchによるcaseの網羅性チェックが入りますのでどちらが良いかは微妙です。
もちろんその方法もdo-try-catchで記述可能です。

エラーを複雑にしてみる 2

複数のtypeのエラーが投げられる場合。

例外が投げられる可能性のあるAPI呼び出しを含むメソッドが独自の例外を投げる可能性もある。という場合などです。
メソッド内でAPI側の例外を処理しろって話ですが、今回はそういうのはなしで。

enum HogeError: Error {
 case notNumber
 case minus
 case lessThan10(Int)
}

struct FugaError: Error {}

func getString() throws -> String {
  throw FugaError()
}

func hoge() throws -> Int {
  switch Int(try getString()) {
  case nil: throw HogeError.notNumber
  case (...0)?: throw HogeError.minus
  case let n? where n < 10 : throw  HogeError.lessThan10(n)
  case let n?: return n
  }
}

HogeErrorに加えてFugaErrorを定義しました。
また文字列を取得するthrowableなgetString()関数を定義し、関数hoge()はそれを利用して文字列を取得するように変更しました。

ではこれをdo - try - catchを使って書いてみます。

do {
  print(try hoge())
}
catch HogeError.notNumber {
  print("not number")
}
catch HogeError.minus {
  print("minus")
}
catch let HogeError.lessThan10(n) {
  print(n, "is less than 10.")
}
catch is FugaError {
  print("Can not get string.")
}
catch {
  print("we caught unknown error.", error)
}

catch is FugaErrorが追加されただけで他は変わっていません。

ではこれをResultを使って書き換えます。

Result<Void, Error> { print(try hoge()) }
  .ifFailure(as: HogeError.self) { error in
    switch error {
    case .notNumber: print("not number")
    case .minus: print("minus")
    case let .lessThan10(n): print(n, "is less than 10.")
    }
  }
  .ifFailure(as: FugaError.self) {
    print("Can not get string.")
  }
  .ifFailure { error in
    print("we caught unknown error.", error)
  }

こちらも.ifFailure(as: FugaError.self)が追加されただけで他は変わっていません。

エラーを複雑にしてみる 3

Errorに継承関係がある場合
ちょっと特殊だとは思いますがErrorに継承関連がある場合を考えます。

class E0: Error {}
class E1: E0 {}

func fuga(_ e1Flag: Bool) throws -> String {
  if e1Flag {
    throw E1()
  }

  throw E0()
}

これをdo - try - catchで書いてみます。

do {
  print(try fuga(true))
}
catch let error as E0 {
  print("E0", error)
}
catch let error as E1 {
  print("E1", error)
}
catch {
  print("Other", error)
}

特に問題はありませんね。
と思いますがこれは問題ありです。
出力結果はこうなります。

E0 E1

投げられた例外はE1です。しかしcatchしたのはE0としてです。
do - try - catchではswitch-caseと同様に記述された順番に検査され最初に合致したcatchのみが実行されそれ以降は無視されます。
E1はE0でもあるためこのような結果を得ることになります。

ただしい書き方はこうなります。

do {
  print(try fuga(true))
}
catch let error as E1 {
  print("E1", error)
}
catch let error as E0 {
  print("E0", error)
}
catch {
  print("Other", error)
}

E1(継承先、子孫)の場合をE0(継承元、親、先祖)の前に記述します。

ではこれをResultで書き換えます。

Result<Void, Error> { print(try fuga(true)) }
  .ifFailure(as: E0.self) { error in
    print("E1", error)
  }
  .ifFailure(as: E1.self) { error in
    print("E1", error)
  }
  .ifFailure { error in
    print("Other", error)
  }

出力結果

E0 E1

先に提示したextensionで実装されたifFailureメソッドのシリーズはメソッドチェーンによる記述となるため記述した順に実行されるのは自明です。
また、一度処理を実行した場合はそれ以降の処理を行わない設計となっていますので、出力結果はdo - try - catchと同様になります。
こちらも以下のように書き換えることで対応します。

Result<Void, Error> { print(try fuga(true)) }
  .ifFailure(as: E1.self) { error in
    print("E1", error)
  }
  .ifFailure(as: E0.self) { error in
    print("E1", error)
  }
  .ifFailure { error in
    print("Other", error)
  }

これで意図した結果が得られます。

まとめ

いかがでしたでしょうか?

適切なextensionを導入することでdo - try - catchを使用するのと遜色なくResultを例外処理機構として利用することができることがわかりました。

結論

だからといってやっちゃだめ。

冒頭に書いたとおり、Swift開発陣は例外は例外として扱いdo - try - catchを使用することを推奨しています。
また、Resultを例外処理機構の一部として利用することを想定していません。

あくまでもネタエントリーであることをご理解いただけますようお願いします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Swiftにおける基本的な型の紹介〜数値型〜

代表的な型とその表現

swiftにはさまざまな型が存在します。
型とは、値の特性と値の操作を表現したものです。

基本的なよく使う型は下記の6個になります。

Int型:整数を表現し、整数を操作するための四則演算などの機能を持っている。
Double型・Float型:浮動小数点数を表す、またInt型と同じく計算の機能を持っている。
String型:文字列を表現し、基本的な文字列操作の機能を持っている。
Bool型:真偽値を表現する。
Optional型:全てに関連する型として、値があるか空なのかのいずれかを表す。
Any型:任意の値を表す。

今回は、この中のInt型ついて記載します。

数値型

数値に関する型を数値型と言ったりするらしいので、まとめさせていただきます。

数値型は大きく分類すると、整数型と浮動小数点型の2つに分けることができます。

・ 整数型

整数型はその名の通り整数を表す型で、ビット数によって様々な型に分けられます。
その中でも代表的な整数型はInt型になります。

ビット数とは二進数の一桁のことです。
16ビットなら、0110101000010100 のように、0もしくは1が16個続いているというイメージだと思います。
コンピュータは、膨大な量の0と1を読み解き動いているんです・・・。恐ろしい!

Int型のビット数は32ビットのプラットフォーム上では32ビット、
64ビットのプラットフォーム上で64ビットとなります。

Int型の他に、固定ビット数の整数型が存在し、
Int8型Int16型Int32型Int64型が存在します。
それぞれのビット数は型についている数字がビット数になります。

それぞれの整数型には扱える整数の幅が存在します。
Int8型:-128 ~ 127
Int16型:-32,768 ~ 32,767
Int32型:-2,147,483,648 ~ 2,147,483,647
Int64型:-9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807

これらの値を超える整数を代入してしまうとコンパイルエラーが発生します。

let a: Int8   // -128 ~ 127
a = 1000   // コンパイルエラー

エラー内容:Integer literal '1000' overflows when stored into 'Int8'
和訳:整数リテラル「1000」は「Int8」に格納されるとオーバーフローします

今回は、Int8をInt16などにすればコンパイルエラーは出ません。

もしかして、Int16とかの許容範囲を覚えなきゃいけないの・・・? と思うかもしれませんが、
基本的にはInt型を使えば大丈夫かなと思います!

ただ、Int32型なども、使う機会があると思うので、型の存在だけでも覚えておいて損はないかなと思いました。

・ 浮動小数点型

浮動小数点型は浮動小数点方式で小数を表す数値型です。

浮動小数点方式とは、ビットを桁の並びを表す仮装部と小数点の位置を表す小数部の2つに分け、
これらを掛けて少数を表す方式だそうです。

正直よくわからない!(笑)

浮動小数点型 = 小数という認識でいいと思います!
つまり、DoubleもFloatも小数を扱う数値型ということですね。

DoubleとFloat どちらを使えばいいのだろうと思うところですが、

Float型はおよそ10の38乗の正負の値まで表すことができ、
Double型はおよそ10の308乗の正負の値まで表すことができます。

さらに、表現できる値の範囲だけでなく、値の精度も大きく異なります。

Double型は最小でも15桁の精度を持ちますが、
Float型は最小で6桁の精度しか持ちません。

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

let a: Double = 12345678.9   // 12345678.9
let b: Float = 12345678.9   // 1.234568e+07

というようになります。
Float型の方は、1.2345678 × 10の7乗ということですね。
Floatで何回も計算をすると、どんどん値の誤差が大きくなってきます。

なので、使うとしたらDouble型一択なのかな?と思いますが、そういうわけでもありません。

例えば、端末の画面上の座標に使用されるCGFloat型は、
32ビットのプラットフォーム上ではではFloat型、
64ビットのプラットフォーム上ではではDouble型になります。

一方で、地球上の座標を表す型などでは、より高い精度が求められるので、
どのプラットフォームでもDouble型が使用されるらしいです。

数値型どうしの変換

swiftでは、異なる型どうしの計算や代入が出来ません。
それは、同じ整数型や浮動小数点型でも同様です。

let a: Int = 123
let b: Int64 = a   //コンパイルエラー

let c: Float = 1.23
let d: Double = c   //コンパイルエラー

数値型を他の数値型に変換するにはイニシャライザを使いキャストします。
各数値型には、他の数値型の値から自動に型を生成するイニシャライザが用意されており、
これらを使用することで数値型どうしの変換が行えます。

let a: Int = 123
let b: Int64 = Int64(a)   // 123

let c: Float = 1.23
let d: Double = Double(c)   // 1.23

また、変換したい型よりも高い精度の型から代入すると、変換した型の精度に合わせて処理が実行されます。

let a: Float = 1.23
let b: Int = Int(a)   // 1

let c: Double = 1.23456789
let d: Float = Float(c)   // 1.234568

数値型の比較

数値型には比較演算という、数値型どうしを比較する演算が存在します。

== : 左辺と右辺が一致する
123 == 456   //false

!= : 左辺と右辺が一致しない
123 != 456   //true

> : 左辺が右辺より大きい
123 > 456    //false

>= : 左辺が右辺以上
123 >= 456   //false

< : 左辺が右辺より小さい
123 < 456    //true

<= : 左辺が右辺以下
123 <= 456   //true

比較演算も計算と同じで型が一致していないと
コンパイルエラーになりますので注意してください。

比較演算はif文で使用しますので覚えておいて損はしないと思います。

以上、ありがとうございました!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Swiftの型推論が原因らしいエラーについての考察

はじめに

Swiftで、map関数を使って
Double型変数をプロパティに持つクラスの配列 → Int型変数をプロパティに持つ構造体の配列
に変換する必要があったのですが、その時に(おそらく)型推論が原因であろうエラーに出会い、かなりハマりました。
頭の中の再整理 & 情報を共有できればと思い、ここに記しておきます。

コード

とあるライブラリを使用する中で遭遇したエラーで、状況はもう少し複雑だったのですが、今回のエラーに関係のないところは削ぎ落として簡略化したコードを以下に書いておきます。

Error
class Origin {
    var x: Double

    init(x: Double) {
        self.x = x
    }
}

struct Mutant {
    var y: Int

    init(y: Int) {
        self.y = y
    }
}

[Origin(x: 100)].map { Mutant(y: $0.x) }
//Error: Value of type 'Any' has no member 'x'

スクリーンショット 2020-11-30 1.27.46.png
以上のように、おそらくSwiftの文法的には何も問題ないのですが
Any型はxというメンバー変数を持っていないよというエラーメッセージが出て怒られてしまっています。
Any型ということは型が定まっていないということですので、map関数の引数$0(ここではOriginクラスの型)に原因があるようです。

Originクラスを定義する時に、変数xはDouble型だよ〜としっかり明示してあるのですが、map関数を使いInt型の配列に変換しようとするとなぜかAny型判定を喰らってしまってうまくいかない...
最初は文法が間違っているのかなと思い色々試行錯誤してみたのですが、何も解決できず...
他のエンジニアの方々に相談して、やっと解決?しました。ありがとうございます?

解決

型を変換しようとしてエラーが出るなら、Mutant構造体のイニシャライザで同じDouble型で受け取ってそこからInt型に変換してはどうか?ということでやってみたところ、この場合はエラーメッセージが出ませんでした。

NotError
class Origin {
    var x: Double

    init(x: Double) {
        self.x = x
    }
}

struct Mutant {
    var y: Int

    init(y: Int) {
        self.y = y
    }

    init(y: Double) {
        self.y = Int(y)
    }
}

[Origin(x: 10)].map { Mutant(y: $0.x) }
//問題なし

型推論がどういう仕組みで動いているのかは全くの未知なのですが、おそらく

1. Swiftが$0の型を推論しようとする
2. Swift「Double型からInt型への変換だから$0がDouble型である可能性は低いな...」
3. Swift「ってことは$0って何型なんだ...?? 分からないからAny型にしとこ...」

という理由で文法的には問題ないにもかかわらずエラーメッセージが出てしまったのだろう.. という結論に至りました。
エラーメッセージが少々不親切だったのではないか感は拭えないですが、気を付けなければいけませんね..
普段何気なくお世話になっている型推論がこんなに奥が深かったとは... と良くも悪くも勉強になったエラーでした。

何かご指摘やご意見ございましたらお気軽によろしくお願い致します。お読みいただきありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む