20191127のSwiftに関する記事は10件です。

一人でVIPERアーキテクチャーでiOSスマホアプリ開発した感想

ここ最近、フルスクラッチのiOSアプリ開発でVIPERアーキテクチャを使用して、開発したので、感想を書いてみます。

はじめに

開発期間が3ヶ月ほどのiOS新規開発アプリで、Swift VIPERアーキテクチャを使用したアプリを開発しました。
途中まで、シンプルなMVCで作成していましたが、いまいちだったのでVIPERを気まぐれで採用した感じです。

いきなり結果ですが、見通しがよく、改修しやすい、品質の高いアプリを実装出来たので、今後も新規開発で採用していこうと思ってます!
あとは、そろそろ誰がガチガチのiOSのフレームワークを作ってくれないかなーと

参考にした資料

VIPERを使って特によかったところ

  • 理解と導入が楽(Clean Architecture / MVVMと比べて)
  • 他のアーキテクチャと比べて、良い意味で奥が深くないと思うので、気楽に導入できる
    (MVC、MVVMは毎回色々悩むので)
  • 慣れると、流れ作業で画面を量産できる
  • 適当に作っても、ルールにしたがっていればそれなりに綺麗にできる
    (ロジックをInteractorにぶち込む)
  • 他の案件に使いまわせる実装がかなり多いイメージ
  • ViewController内に、画面遷移のコードを書かなくて良いのがすごくかっこいい
  • ロジック、WebAPIの仕様変更があっても、ViewControllerまで変更が入ることが少なかった

メリット詳細

  • プロジェクト構成を悩まなくて良い
  • MVCで開発する速度と、慣れさえすれば変わらないと思う
  • ViewControllerを徹底的に軽くできる
  • APIの仕様変更時に、ViewControllerを変更しなくて良い確率が上がる
  • 途中まで、MVCで作っていた物を、さほど苦戦せずにVIPERに変更できた
  • MVVMとは違い、ロジックの処理をまとめやすい
  • 開発が進むにつれて、開発速度をどんどん上げることが出来た
  • 開発が進んでも一定の品質を簡単に保てた
  • 複数人開発の時に、基準ができるので無駄な争いが起きなそう
  • 各階層ごとの役割が明快なので、コードレビューもしやすいはず
  • エラー表示処理や、インジゲータ表示処理などをViewControllerのExtensionにまとめると、使いまわしがすごくしやすい
  • 無駄な条件分岐の実装が極端に減った
  • プログラム内のネストが極端に減った
  • 多分、UTもしやすいはず(そんな工数はもらってないので、やってない)

デメリット詳細

MVCと比べて、開発速度が下がると行ったこともほとんどないので、デメリットはほとんど無しです。

  • 開発の初期は、プロジェクトの構成作りに悩んでしまったので、時間を浪費した
  • 当たり前だが、引き継ぎ時にアーキテクチャを相手に理解してもらう必要が出てきた
  • 日本では、RxSwiftを使ったMVVMの方が流行ってるイメージ

悩んだところ

  • 色々なサンプルをみたが、割とみんなバラバラなプロジェクト構成、作りになっている
    -> PHPのフレームワークのようにガチガチな規約があると考えなくて助かるのになぁと
  • Interactorを使い回す時の取り回し
    -> DIする時にまとめて渡す方式にした
  • Protocolの置き場所
    -> モジュールごとに1ファイルにまとめた
  • View -> Presenter部分のメソッド名の付け方
    -> 結局アクション名でつけた 例)didPushHoge, didSelectHoge

失敗したところ

  • ViewControllerに少しロジックが乗ってしまった
  • 前の画面に戻る遷移は、全RouterにInterfaceでつければよかった
  • モジュールの自動作成をできるようにすれば、もっと工数を下げれた

次回以降、気をつけたいところ、やりたいこと

  • 公開できるレベルの実際に動作するサンプルPJを作りたい
  • VIPER + Rxでいい感じになるかも
  • View -> Presenter部分のメソッド名の付け方を再考する
  • モジュールのテンプレをコマンドで作成できるようにする
  • RouterのViewController生成部分でNavigationControllerを渡したのはよくない

おまけ(似たようなアーキテクチャ感想記事)

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

一人でVIPERアーキテクチャーでiOSスマホアプリ開発した感想 まとめ

ここ最近、フルスクラッチのiOSアプリ開発でVIPERアーキテクチャを使用して、開発したので、感想を書いてみます。

はじめに

開発期間が3ヶ月ほどのiOS新規開発アプリで、Swift VIPERアーキテクチャを使用したアプリを開発しました。
途中まで、シンプルなMVCで作成していましたが、いまいちだったのでVIPERを気まぐれで採用した感じです。

いきなり結果ですが、見通しがよく、改修しやすい、品質の高いアプリを実装出来たので、今後も新規開発で採用していこうと思ってます!
あとは、そろそろ誰がガチガチのiOSのフレームワークを作ってくれないかなーと

参考にした資料

VIPERを使って特によかったところ

  • 理解と導入が楽(Clean Architecture / MVVMと比べて)
  • 他のアーキテクチャと比べて、良い意味で奥が深くないと思うので、気楽に導入できる
    (MVC、MVVMは毎回色々悩むので)
  • 慣れると、流れ作業で画面を量産できる
  • 適当に作っても、ルールにしたがっていればそれなりに綺麗にできる
    (ロジックをInteractorにぶち込む)
  • 他の案件に使いまわせる実装がかなり多いイメージ
  • ViewController内に、画面遷移のコードを書かなくて良いのがすごくかっこいい
  • ロジック、WebAPIの仕様変更があっても、ViewControllerまで変更が入ることが少なかった

メリット詳細

  • プロジェクト構成を悩まなくて良い
  • MVCで開発する速度と、慣れさえすれば変わらないと思う
  • ViewControllerを徹底的に軽くできる
  • APIの仕様変更時に、ViewControllerを変更しなくて良い確率が上がる
  • 途中まで、MVCで作っていた物を、さほど苦戦せずにVIPERに変更できた
  • MVVMとは違い、ロジックの処理をまとめやすい
  • 開発が進むにつれて、開発速度をどんどん上げることが出来た
  • 開発が進んでも一定の品質を簡単に保てた
  • 複数人開発の時に、基準ができるので無駄な争いが起きなそう
  • 各階層ごとの役割が明快なので、コードレビューもしやすいはず
  • エラー表示処理や、インジゲータ表示処理などをViewControllerのExtensionにまとめると、使いまわしがすごくしやすい
  • 無駄な条件分岐の実装が極端に減った
  • プログラム内のネストが極端に減った
  • 多分、UTもしやすいはず(そんな工数はもらってないので、やってない)

デメリット詳細

MVCと比べて、開発速度が下がると行ったこともほとんどないので、デメリットはほとんど無しです。

  • 開発の初期は、プロジェクトの構成作りに悩んでしまったので、時間を浪費した
  • 当たり前だが、引き継ぎ時にアーキテクチャを相手に理解してもらう必要が出てきた
  • 日本では、RxSwiftを使ったMVVMの方が流行ってるイメージ

悩んだところ

  • 色々なサンプルをみたが、割とみんなバラバラなプロジェクト構成、作りになっている
    -> PHPのフレームワークのようにガチガチな規約があると考えなくて助かるのになぁと
  • Interactorを使い回す時の取り回し
    -> DIする時にまとめて渡す方式にした
  • Protocolの置き場所
    -> モジュールごとに1ファイルにまとめた
  • View -> Presenter部分のメソッド名の付け方
    -> 結局アクション名でつけた 例)didPushHoge, didSelectHoge

失敗したところ

  • ViewControllerに少しロジックが乗ってしまった
  • 前の画面に戻る遷移は、全RouterにInterfaceでつければよかった
  • モジュールの自動作成をできるようにすれば、もっと工数を下げれた

次回以降、気をつけたいところ、やりたいこと

  • 公開できるレベルの実際に動作するサンプルPJを作りたい
  • VIPER + Rxでいい感じになるかも
  • View -> Presenter部分のメソッド名の付け方を再考する
  • モジュールのテンプレをコマンドで作成できるようにする
  • RouterのViewController生成部分でNavigationControllerを渡したのはよくない

おまけ(似たようなアーキテクチャ感想記事)

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

DelegateProxy の Observable にイベントが飛んでこないケースがあった

RxSwift (RxCocoa) には、標準ライブラリの主要な delegate メッセージに対応する Observable が用意されています。DelegateProxy という仕組みです。

今回、その中で UISearchControllerDelegate の各デリゲートメソッドを、RxSwift を用いて非同期的に扱いたかったのですが、ハマったのでまとめておきます。

うまくいかないパターン

class TheViewController: UIViewController, UISearchControllerDelegate {

    var searchController = UISearchController()
    let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        searchController.delegate = self

        // 1
        searchController.rx.willPresent.subscribe(onNext: { _ in
            /** これが呼ばれない **/
        }).disposed(by: disposeBag)
    }

    // 2
    func willPresentSearchController(_ searchController: UISearchController) {
        /** こっちは呼ばれる ***/
    }
}

上記の例では、UISearchControllerDelegate を、TheViewController 自身にセットして、willPresentSearchController を実装していますが、同時に RxSwift を使って willPresentSearchController の Observable 変数である willPresent に何らかの処理を非同期で行おうとしています。

こうすると、willPresent を subscribe しても実際にここにイベントが飛んでくることはありませんでした。

1 と 2 は同じタイミングで発火するものですが、delegate が viewController にセットされていることで 2 が優先されているといった状況になっているようです。

===
※ 実際には1つのクラス内でこんな妙な実装をすることはなく、プロトコルなどを使ってデリゲートの宣言や共通処理を切り出していたりするので、この問題のパターンになっていることに気付きませんでした。

うまくいくパターン

class TheViewController: UIViewController {

    var searchController = UISearchController()
    let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        // 1
        searchController.rx.willPresent.subscribe(onNext: { _ in
            /** 呼ばれる **/
        }).disposed(by: disposeBag)
    }
}

上記サンプルを見れば当たり前なのですが...

RxSwift の DelegateProxy で与えられる Observable を購読して非同期で処理したい場合は、それに集約する必要があります。つまり UIViewControllerDelegate を ViewController に指定して、デリゲートメソッドを実装する方法を同時に行わないということです。

親にあたるクラスやプロトコルでデリゲートが実装されていると、末端のクラスでこのように RxSwift の DelegateProxy 経由の Observable が飛んでこないことがあります。
ソースコードが大規模になってくるとプロトコルや継承によって見通しが悪くなると起こりやすいので、気を付ける必要がありそうです。

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

Dictionaryをfilterするとき、別の配列から参照して絞り込む方法

contents = [
    ["name": "banana"
    "id": 1],
    ["name": "cabbage"
    "id": 2],
    ["name": "tomato"
    "id": 3],
    ["name": "strawberry"
    "id": 4],

]

contentsId = [1, 3]

こんな配列があった時、contentsIdに含まれるIDのものだけ取り出したい時

filted = content.filter({  contentsId.contains($0.id) })

 [
    ["name": "banana"
    "id": 1],
    ["name": "cabbage"
    "id": 3],

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

if文をメソッドチェーンにしてしまう

じょ

何言ってるんだ?というタイトルですがこういうことです。

let a = 0
ifTrue(a == 0) { print("0") }
    .elseIf(a == 1) { print("1") }
    .else { print("other") }

先日メソッドチェーン原理主義という言葉を思いつきまして、メソッドチェーン原理主義者であれば制御構文もメソッドチェーンでなければ許されないのではないかと考え作ってみました。

賢明な皆さまはもうお気づきだと思いますが、このエントリは「特に何の意味もない」エントリです。戯れ言には付き合わないタイプの方はすぐさまお引き取り頂ければ幸いです。

なお、以下のコードはすべてSwift5.1で書かれています。

最初の実装

メソッドチェーンを行うためにはチェーンを繋ぐ「糊」に当たるものが必要です。今回はこんな感じです。

enum IfResult {
    case `true`, `false`
}

もう既にバカっぽさ全開です。
メソッド群は条件が真のときはIfResult.trueを偽の時はIfResult.falseを戻すように作成します。

入口

IfResultを使って早速メソッドチェーンの起点となる関数を作成します。

private func doAndTrue(_ f: () -> Void) -> IfResult {
    (f() is Void) ? .true : .false
}

func ifTrue(_ condition: @autoclosure () -> Bool, _ excute: () -> Void) -> IfResult {
    condition() ? doAndTrue(excute) : .false
}

頭のおかしすぎる関数が混ざっていますが、ifTrue(_:_:)自体は難しくはないですね。
当然のようにdoAndTrue(_:)関数で警告が出ていますが無視しましょう!(後で消えてなくなるのでご安心ください)

繋げる

起点はできましたのでIfResultを拡張してメソッドチェーンが作れるようにしていきます。

enum IfResult {
    case `true`, `false`

    @discardableResult
    func elseIf(_ condition: @autoclosure () -> Bool, _ excute: () -> Void) -> Self {
        self == .false && condition() ? doAndTrue(excute) : self
    }
}

IfResultを戻したメソッドの条件が真であった場合は以降の処理はすべてキャンセルされます。
ですので、IfResultfalseの時だけ条件を検査し処理するようにします。

さらにもう一つ

enum IfResult {
    case `true`, `false`

    func `else`(_ excute: () -> Void) {
        self == .false ? excute() : ()
    }
}

これは簡単ですね。

できた

では冒頭で提示したメソッドチェーンを実行してみましょう

func f(_ a: Int) {
    ifTrue(a == 0) { print("0") }
        .elseIf(a == 1) { print("1") }
        .else { print("other") }
}

(0...3).forEach(f)
// prints ...
// 0
// 1
// other
// other

ちゃんと動いてますね。

改良された実装

ちゃんと動いてはいますが本当にこれでいいでしょうか?
else(_:)メソッドが何も戻さないためメソッドチェーンはここで途切れることになります。
次につながるようになっていた方がよさそうです。では次に何を渡せばいいでしょう。
やはり何らかの値を伝播したいですね。
例えば

let a: String = ifTrue(cond) { "なにか" }.else { "なんだ?" }

のような感じです。

メソッドチェーンを次につなげるために先の実装を改良していきましょう。

enum IfResult<Value> {
    case `false`
    case value(Value)

    struct IfResultError: Error {}
    func get() throws -> Value {
        guard case let .value(v) = self else { throw IfResultError() }
        return v
    }
}

値を持てるようにしました。
またget()メソッドで値そのものを取り出せるようにしました。

入口

ifTrue(_:_:)メソッドを改修します。

func ifTrue<T>(_ condition: @autoclosure () -> Bool, _ excute: () -> T) -> IfResult<T> {
    condition() ? .value(excute()) : .false
}

不格好なdoAndTrue(_:)が不要になりすっきりしました。

繋げる

elseIf(_:_:)else(_:)もサクッと実装します。
再掲になる部分は除いています

enum IfResult<Value> {
    case `false`
    case value(Value)

    @discardableResult
    func elseIf(_ condition: @autoclosure () -> Bool, _ excute: () -> Value) -> Self {
        switch self {
        case .false: return condition() ? .value(excute()) : .false
        case .value: return self
        }
    }

    @discardableResult
    func `else`( _ excute: () -> Value) -> Self {
        switch self {
        case .false: return .value(excute())
        case .value: return self
        }
    }
}

ちょっと長くなってしまいましたがごく素直な実装です。

できた

新しいメソッドチェーンを見てみましょう。

func f(_ a: Int) -> IfResult<String> {
    ifTrue(a == 0) { "0" }
        .elseIf(a == 1) { "1" }
        .else { "other" }
}

(0...3)
    .map(f)
    .map { r in Result { try r.get() } }
    .forEach { _ = $0.map { print($0) } }
// prints ...
// 0
// 1
// other
// other

問題なく動いています。
完成です。

拡張された実装

ホントに完成?
値を取り出すのにResultに頼っています。これはちょっとどうかと思います。
なので実装を拡張します。

もなど

extension IfResult {
    @discardableResult
    func map<T>(_ transform: (Value) -> T) -> IfResult<T> {
        if case let .value(v) = self { return .value(transform(v)) }
        return .false
    }

    @discardableResult
    func flatMap<T>(_ transform: (Value) -> IfResult<T>) -> IfResult<T> {
        if case let .value(v) = self { return transform(v) }
        return .false
    }
}

利用例

func fizzBuzz1(_ i: Int) -> IfResult<String> {
    ifTrue(i < 1) { fatalError("あかんて") }
        .elseIf(i.isMultiple(of: 3) && i.isMultiple(of: 5)) { "FizzBuzz" }
        .elseIf(i.isMultiple(of: 3)) { "Fizz" }
        .elseIf(i.isMultiple(of: 5)) { "Buzz" }
        .else { String(i) }
}

(1...100)
    .map(fizzBuzz1)
    .forEach { $0.map { print($0) } }
_人人人人人人人人人_
> 突然のFizzBuzz <
 ̄Y^Y^Y^Y^Y^Y^Y^Y^Y^ ̄

比較

ついでなのでEquatableにしておきましょう。
Valueの型がEquatableでないと比較することができないので制約付きのextensionになります。

extension IfResult: Equatable where Value: Equatable {
    static func == (lhs: Self, rhs: Self) -> Bool {
        switch (lhs, rhs) {
        case (.false, .false): return true
        case let (.value(v1), .value(v2)): return v1 == v2
        default: return false
        }
    }
}

まとめ

突然の思い付きにしてはよくできたのではないでしょうか?
関数は可能な限り1つのメソッドチェーンで完結するように書いています。
いくつか難しいものがあり妥協がたくさんありました。
真のメソッドチェーン原理主義者様ならそれらもメソッドチェーンで書くのでしょう。

告白

最後に真実を告白させていただきます。
実は私はメソッドチェーン原理主義者ではありません。
ですのでこのエントリで作成したメソッド群は使うことはないでしょう。
使う人がいるのかも疑問です。

ちゃんと最初に「特に何の意味もない」戯れ言だって断ってるからね?
怒っちゃだめよ。

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

Swiftの関数内関数

Swiftで関数内に関数を書く。

func abc() {
    func test() {
        print("内部関数だよ")
    }
    test()
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】画面遷移の方法(俯瞰用)

値の受け渡しや求めるUIに合わせた記述の仕方など、単純に見えてバリエーションの多い画面遷移についてまとめます。
performSegueやら、popViewControllerやら、dismissやら、InterfaceBuilderやら、コーディングやら地味にバリエーションが多くてめんどくさくないですか?
今まで経験則でなんとなく使ってきましたが、詰まってはググってを繰り返していたので一度使い方をまとめたいと思います。
備忘録的な記事になりますが、ご容赦願います。

画面遷移で混乱すること

  • コーディングとかInterfaceBuilderとか実装方法がいくつかあって忘れる
  • 遷移の種類がいくつかあって混乱する(showとかmodalとか)
  • 画面間の値の受け渡しでコケる
  • 画面から戻るときにコケる
  • 上記の組み合わせで実装方法が違って泣く

僕は上記のようなことでよく詰まります。
ですので、以下ではこの辺をなんとなく整理できたらなと思います。

画面遷移の実装方法

画面遷移の実装方法は主に以下の三つ。
A. InterfaceBuilder(ボタンと画面をSegueで接続)
B. InterfaceBuilder(Segue Identifier) + コーディング
C. InterfaceBuilder(Storyboard ID) + コーディング

ボタンを押したら遷移するだけなどの簡単な処理はAパターンで良いとして、
多くの場合はボタン押下時になんらかの処理を実行するケースが多いと思います。
それに伴いBパターン、Cパターンもしっかり使えるようになっておく必要があります。

B. InterfaceBuilder(Segue Identifier) + コーディング

遷移元・先のViewControllerをSegueで繋げた後に、SegueのIdentifierを設定。
その際に画面遷移の種類(Show,Present Modalyなど)を指定することができます。
これにより遷移したいタイミングで

performSegue(withIdentifier : "toNextViewController", sender: nil)

などとすることで遷移を実行できます。
ちなみにsenderによってボタンからの遷移実行なのかtapGestureRecognizerなのか、segueの出し元の判定ができます。

C. InterfaceBuilder(Storyboard ID) + コーディング

遷移先のStoryboardにIDを振り、コードでそのViewControllerを呼び出します。
実際のコード例は以下。

// Storyboard、ViewControllerを指定
// 同じStoryboard上であればselfを用いて指定
let storyboard: UIStoryboard = self.storyboard!
let next: UIViewController = storyboard.instantiateViewController(withIdentifier:"nextViewController") as! UIViewController
// 遷移処理
navigationViewController.pushViewController(next, animated: true, completion: nil)

Storyboard IDを利用して遷移先のViewControllerのインスタンスを取得した後に、遷移先の処理を実行します。

画面遷移の種類

画面遷移のバリエーションは5つ?くらいあるけど、実際よく使うのは、
ShowPresent Modaly の二つと思われます。
一旦それ以外の遷移の種類は忘れることとして、それぞれについてまとめます。

Show

遷移元の画面から横にズイっと新しい画面が現れる遷移。
NavigationControllerの管理下で使うことが多いです。
rootViewControllerに対して次々にShowで画面を繋げた場合、
Showで繋げたviewcontrollerはrootViewControllerの子となります。

NavigationController管理下における、
A_ViewController -> B_ViewControllerへの遷移では、
pushViewControllerメソッドを用いて、showの遷移が可能です。
(先ほどCパターンの実装例であげた実装例)

AViewContoller.swift
let next: UIViewController = storyboard.instantiateViewController(withIdentifier:"nextViewController") as! B_ViewController
navigationViewController.pushViewController(next, animated: true, completion: nil)

また、遷移先のB_ViewControllerから遷移元に戻るときは、popViewControllerメソッドを使うことで遷移元に戻ることができます。

BViewContoller.swift
navigationViewController.popViewController(animated: true)

Present Modaly

Viewの上に覆いかぶさるような形で下から新たなViewが現れる遷移。
一時的なユーザーの操作受付やオンボーディングからアプリの本機能への遷移時など、つながりをあえて切りたいときによく使われます。
iOS13以降ではデフォルトの遷移がフルスクリーンではなく、Safariのタブのような重なり方となっており、必要に応じてフルスクリーンの設定が必要になります。

このパターンでも同様に、
A_ViewController -> B_ViewControllerへの遷移を考えたとき、
遷移元からは

A_ViewContoller.swift
let next: UIViewController = storyboard.instantiateViewController(withIdentifier:"nextViewController") as! B_ViewController
self.present(next,animated: true, completion: nil)

にて遷移ができ、遷移先からは

B_ViewContoller.swift
self.dismiss(animated: true, completion: nil)

dismissメソッドにて遷移元に戻ることが可能です。

まとめ

今回画面遷移の方法について、実装方法・代表的な遷移の種類・各メソッドの位置付けをまとめました。
今回書ききれなかった画面間の値渡しや、画面遷移時の処理など、時間のあるときに書きたいと思います。
見ていただいた方、ありがとうございました。

参考

【iOS】画面遷移方法まとめ
【Swift/iOS】遷移元画面への戻り方
【Swift/iOS】Segueを使わない画面遷移

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

swift UTF8からSJISに変換するときに、数値文字参照も使用する

問題 文字コード変換だけでは望ましい結果にならない

具体的にはしたらば掲示板(EUC-JP)や旧2ちゃんねる互換掲示板(ShiftJIS)を想定しています。

ShiftJISのウェブサーバにPOSTしたいのですが、変換できない文字があると
String.data(using: .shiftJIS)だと失敗します。

String.data(using: .shiftJIS, allowLossyConversion: true)
を使うと失敗はしませんが変換できなかった部分が?になって「何これ?」ってなります。

解決方法

  1. 1文字ごとにShiftJIS(EUC-JP)に変換できるか確認する
  2. 変換できない場合にはhtmlの文字参照に起きかえる
  3. ShiftJIS(EUC-JP)変換を行なう というステップを取ります。
var postStr: String = "(´・ω・`)"
let postStrChaRef: String = postStr.characterReferenceSJIS() // 文字参照変換
let postData:Data? = postStrChaRef.data(using: .shiftJIS)  // SJIS変換

import Foundation
extension String {
    func characterReferenceSJIS() -> String {
        var buf = ""
        for c in self.map({ String($0) }) {
            if c.canBeConverted(to: String.Encoding.shiftJIS) {
                buf.append(c)
            } else {
                let buf1 = NSMutableString(string: c)
                CFStringTransform(buf1, nil, kCFStringTransformToXMLHex, false)
                buf.append(buf1 as String)
            }
        }
        return buf
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift/iOS】CoreLocationで取得した位置情報を住所に変換する

概要

下記の記事で説明した位置情報から自然な住所の表記に変換する方法について説明します。
【Swift/iOS】CoreLocationで緯度・経度を取得して位置情報を表示する

位置情報を取得した場所によって地名として取得される情報の粒度が異なるため、地名をつなげるだけでは自然な住所表記にならない場合があります。
具体例としては、下記の実行結果をご覧ください。

確認バージョン

Xcode 11.2.1
Swift 5.1.2
iOS 13.2.2

要件

  1. 地名が丁目や番地まで取得できる場所では丁目や番地までの住所を出力
  2. 地名が「〜通り」や「〜道路」などの場合には丁目なしの住所を出力
  3. 「〜通り」や「〜道路」は住所に含めない

結論

下記のように実装すると要件を満たす住所が出力されました。

//位置情報
let administrativeArea = placemark.administrativeArea == nil ? "" : placemark.administrativeArea!
let locality = placemark.locality == nil ? "" : placemark.locality!
let subLocality = placemark.subLocality == nil ? "" : placemark.subLocality!
let thoroughfare = placemark.thoroughfare == nil ? "" : placemark.thoroughfare!
let subThoroughfare = placemark.subThoroughfare == nil ? "" : placemark.subThoroughfare!
let placeName = !thoroughfare.contains( subLocality ) ? subLocality : thoroughfare

//住所
let address = administrativeArea + locality + placeName + subThoroughfare

説明

プロパティ名 概要 説明
administrativeArea 都道府県 そのまま(nilにはならないはず)
subAdministrativeArea 町村の場合、localityに〜郡〜町/村と出力されるのでこちらは使用しません
locality 市区町村 そのまま(nilにはならないはず)
subLocality 丁番なしの地名 thoroughfareにsubLocalityが含まれない場合はsubLocality
thoroughfare 地名 上記以外はthoroughfare
subThoroughfare 番地 nilの場合は空白

実行結果

出力される住所は下記の画面の最終行に表示されています。

永田町2丁目 永田町 日比谷公園 奥多摩町
スクリーンショット 2019-11-26 23.49.06.png スクリーンショット 2019-11-26 23.58.09.png スクリーンショット 2019-11-26 23.53.09.png スクリーンショット 2019-11-26 23.51.56.png

実装

  1. 情報表示用のUILabelを追加
  2. 位置情報を利用許可状態を判定
  3. 位置情報の更新開始
  4. CLLocationデータを逆ジオコーディングして位置情報を取得
  5. 位置情報を住所に変換
  6. 位置情報が更新されるごとにUILabelに設定
import UIKit
import CoreLocation

class ViewController: UIViewController, CLLocationManagerDelegate {
    let locationManager = CLLocationManager()
    let geocoder = CLGeocoder()
    let text = [ "緯度", "経度", "国名", "郵便番号", "都道府県", "郡", "市区町村", "丁番なしの地名", "地名", "番地" ]
    var item: [ UILabel ] = []
    var location: [ UILabel ] = []
    var address: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()

        //サイズ
        let width = self.view.frame.width / 2
        let height = self.view.frame.height / CGFloat( self.text.count + 2 )

        //ラベル
        for ( i, text ) in self.text.enumerated() {
            //項目
            self.item.append( UILabel() )
            self.item.last!.frame.size = CGSize( width: width, height: height )
            self.item.last!.frame.origin = CGPoint( x: 0, y: height * CGFloat( i + 1 ) )
            self.item.last!.textAlignment = .center
            self.item.last!.text = text
            self.view.addSubview( self.item.last! )

            //データ
            self.location.append( UILabel() )
            self.location.last!.frame.size = CGSize( width: width, height: height )
            self.location.last!.frame.origin = CGPoint( x: width, y: height * CGFloat( i + 1 ) )
            self.location.last!.textAlignment = .center
            self.view.addSubview( self.location.last! )
        }

        //住所
        self.address = UILabel()
        self.address.frame.size = CGSize( width: self.view.frame.width, height: height )
        self.address.frame.origin = CGPoint( x: 0, y: height * CGFloat( self.text.count + 1 ) )
        self.address.textAlignment = .center
        self.view.addSubview( self.address )

        //ロケーションマネージャ
        self.locationManager.requestWhenInUseAuthorization()
        let status = CLLocationManager.authorizationStatus()
        if status == .authorizedWhenInUse {
            self.locationManager.delegate = self
            self.locationManager.distanceFilter = 10
            self.locationManager.startUpdatingLocation()
        }
    }

    func locationManager( _ manager: CLLocationManager, didUpdateLocations locations: [ CLLocation ] ) {
        //表示更新
        if let location = locations.first {
            //緯度・経度
            self.location[0].text = location.coordinate.latitude.description
            self.location[1].text = location.coordinate.longitude.description

            //逆ジオコーディング
            self.geocoder.reverseGeocodeLocation( location, completionHandler: { ( placemarks, error ) in
                if let placemark = placemarks?.first {
                    //位置情報
                    self.location[2].text = placemark.country
                    self.location[3].text = placemark.postalCode
                    self.location[4].text = placemark.administrativeArea
                    self.location[5].text = placemark.subAdministrativeArea
                    self.location[6].text = placemark.locality
                    self.location[7].text = placemark.subLocality
                    self.location[8].text = placemark.thoroughfare
                    self.location[9].text = placemark.subThoroughfare

                    //住所
                    let administrativeArea = placemark.administrativeArea == nil ? "" : placemark.administrativeArea!
                    let locality = placemark.locality == nil ? "" : placemark.locality!
                    let subLocality = placemark.subLocality == nil ? "" : placemark.subLocality!
                    let thoroughfare = placemark.thoroughfare == nil ? "" : placemark.thoroughfare!
                    let subThoroughfare = placemark.subThoroughfare == nil ? "" : placemark.subThoroughfare!
                    let placeName = !thoroughfare.contains( subLocality ) ? subLocality : thoroughfare
                    self.address.text = administrativeArea + locality + placeName + subThoroughfare
                }
            } )
        }
    }
}

Githubのソースコードはこちら

緯度・経度の確認方法について

住所から緯度・経度を確認するのに下記のサイトを使用しました。
いろいろな住所で変換結果が正しいことを確認するのに重宝しました。
https://www.geocoding.jp

このサイトで検索した緯度・経度を下記のように入力すると、その場所での位置情報を確認することができます。

//緯度・経度
let location = CLLocation( latitude: 35.675775, longitude: 139.743488 )

//逆ジオコーディング
self.geocoder.reverseGeocodeLocation( location, completionHandler: {
   //以下、略

参考

【CoreLocation】位置情報を取得する
[iOS] MapKitを使って”ジオコーディング・逆ジオコーディング”をやってみる

開発アプリ

私が開発したアプリを紹介します。
この記事とは直接関係ないのですが、興味があればダウンロードしてもらえると嬉しいです。
NomiReco - お酒好きなあなたをサポート

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

SwiftUIのTabViewの問題に自作TabView(SwiftUI製)で対処する

SwiftUIのTabViewの問題に自作TabView(SwiftUI製)で対処する

 

SwiftUIのTabViewには、Tabを切り替えると切り替えた先のタブを再生成するためスクロールした位置や状態を保持しておくことができませんでした。

それに対処するために、今回自作TabViewを作成しましたので、その tipsを共有します。

作成したTabViewを見たい方はこちらから!
https://github.com/tsuzukihashi/SwiftUI_CustomTabBar

また、UIKitのUITabBarControllerを Representaぶる方法もあります。そっちの方がiOS的には優しいかもしてません...

こっちのメリットは、タブ切り替えのアニメーションがいじりやすいのと、オールSwiftUIのモチベぐらいでしょうか...

間違いやアドバイスがあればじゃんじゃんコメントください!お願い致します?‍♂️

Twitter やっています!
https://twitter.com/tsuzuki817

色々アプリをリリースしているので一度見てみてください?‍♂️
https://apps.apple.com/jp/developer/ryo-tsudukihashi/id1320583602?l

現状の問題を把握

先ほど説明しましたが、改めてGIFアニメーション載せておきます。
2tdvx-fuo22.gif

自作TabViewへの道

大まかな流れは以下のようになっています。

  • ステップ1 ルートのViewで現在選択しているcurrentPageを保持する
  • ステップ2 ルートのViewでタブで遷移したいViewをZStackで並べる
  • ステップ3 ステップ2で配置したViewのopacityをcurrentPageによって変える
  • ステップ4 タブもどきに設置したボタンでcurrentPageを切り替える

説明しやすいように、シンプルなアプリを作成します。

表示させたいView達

変化が分かりやすいように縦に多くスクロールできるViewを作成しています。

Page2では天下のイラストや様の画像を少しばかりお借りしております。

import SwiftUI
struct Page1: View {
    var body: some View {
        NavigationView {
            VStack {
                List(0..<30) { i in
                    Text("\(i)列目")
                }
            }.navigationBarTitle("1ページ目")
        }
    }
}

struct Page2: View {
    var body: some View {
        NavigationView {
            ScrollView(.vertical, showsIndicators: false) {
                ForEach(1..<8) { i in
                    Image("irasutoya\(i)")
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                }
            }.navigationBarTitle("2ページ目")
        }
    }
}

struct Page3: View {
    var body: some View {
        NavigationView {
            VStack {
                List(0..<30) { i in
                    Text("\(i)列目")
                }
            }.navigationBarTitle("3ページ目")
        }
    }
}

ルートのView

このViewで@State で currentPageを保持しておきます。
そのcurrentPageの値を見てViewのopacityを変更して、表示・非表示を切り替えています。

また、ここでoffsetをいじっているのですが、これはアニメーションのために利用しています。

そのためアニメーションを利用しない方は、なくてもおkです!
(.rotation3DEffectとか使ったらリッチになって面白そうだったのですが、navigationbar が少しおかしくなりました;;)

import SwiftUI

struct ContentView: View {
    @State  var currentPage = 1

    var body: some View {
        ZStack {
            Page1()
                .opacity(currentPage == 1 ? 1 : 0)
                .offset(x : currentPage == 1 ? 0 : 100)
                .animation(Animation.spring())

            Page2()
                .opacity(currentPage == 2 ? 1 : 0)
                .offset(x : currentPage == 2 ? 0 : 100)
                .animation(Animation.spring())

            Page3()
                .opacity(currentPage == 3 ? 1 : 0)
                .offset(x : currentPage == 3 ? 0 : 100)
                .animation(Animation.spring())

            MyTabView(currentPage: $currentPage)
        }
    }
}

肝心の自作TabView

@BindingでcurrentPageをルートViewから受け取り、設置するボタンでその値を変更します。

VStack, Spacer() , ZStack, Rectangleを駆使して擬似タブを作成します。

.edgesIgnoringSafeArea(.bottom)を使って、セーフエリアまで覆うのを忘れないようにしておけば、後は結構自由です。

struct MyTabView: View {
    @Binding var currentPage: Int
    var width = UIScreen.main.bounds.width
    var body: some View {
        VStack {
            Spacer()
            ZStack {
                Rectangle()
                    .foregroundColor(Color.gray)
                    .frame(width: width, height: 88)

                HStack(spacing: 88) {
                    Button(action: {
                        self.currentPage = 1
                    }) {
                        VStack {
                            Image(systemName: "flame")
                                .foregroundColor(Color.red)
                                .font(.system(size: 32, weight: .bold, design: .rounded))

                        }
                    }.padding(.bottom, 24)

                    Button(action: {
                        self.currentPage = 2
                    }) {
                        VStack {
                            Image(systemName: "bolt")
                                .foregroundColor(Color.yellow)
                                .font(.system(size: 32, weight: .bold, design: .rounded))

                        }
                    }.padding(.bottom, 24)

                    Button(action: {
                        self.currentPage = 3
                    }) {
                        VStack {
                            Image(systemName: "leaf.arrow.circlepath")
                                .foregroundColor(Color.green)
                                .font(.system(size: 32, weight: .bold, design: .rounded))

                        }
                    }.padding(.bottom, 24)

                }
            }
        }.edgesIgnoringSafeArea(.bottom)
    }
}

できたもの

6t67u-qqz8z.gif

タブを切り替えてもViewは作り直されず、遷移前の状態を保持することができました!

最終的な着地点

以上で実装は終わりです!

SwiftUIは色々不便なことも多いですが、楽しいですね!

そのうちTabViewの問題もApple様が直してくれそうですが、一時的な場しのぎとしてどうでしょうか

最後にもう一度作成したリポジトリです!
https://github.com/tsuzukihashi/SwiftUI_CustomTabBar

閲覧ありがとうございましたmm

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