20200814のSwiftに関する記事は9件です。

新しいmapを使ってみた

はじめに

Apple Developer DocumentationをみるとMapKit中にstruct Mapが増えています。
どうもSwiftUIで直接mapが表示できるようになったようです。
実際に作ってみました。
サンプルプロジェクトはgithubにアップしてあります。

コードの解説

サンプルプロジェクトのポイントを紹介します。

Mapを表示する

@State var coordinate = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 36.0, longitude: 138), latitudinalMeters: 2000000, longitudinalMeters: 2000000)
Map(coordinateRegion: $coordinate)

Mapを表示するには、表示したい位置情報(coordinate)を指定すると表示できます。

ピンを表示する

ピンを表示するには、MapAnnotationProtocolを実装する必要があるようです。
今回は、下記のような構造体を用意しました。(ネーミングが悪くてすみません)

struct AnnotationItemStruct:Identifiable{
    // ID(識別子)
    let id = UUID()
    // 緯度経度
    let coordinate:CLLocationCoordinate2D
}

次にピンの配列を作成します。

@State var annotationItems: [AnnotationItemStruct] = []

最後にピンに位置情報を追加します。

let annotationItem = AnnotationItemStruct(coordinate: coordinate)
self.annotationItems.append(annotationItem)

実際にピンをmapに表示できるようにmapを修正します。

Map(coordinateRegion: $coordinate, annotationItems: annotationItems) { annotationItem in
    // annotationItemsから1つ取りだした情報からピンを打つ
    MapPin(coordinate: annotationItem.coordinate)
}

参考

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

CocoaPodsインストール時に発生する「ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES」の意味と対応方法2つ

はじめに

CocoaPodsをインストールしたときに、「ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES」に関する警告が表示されることはありませんか?
この記事では、その意味と対応方法を2つご紹介します。

発生する事象

pod install を実行した場合、以下のようなメッセージが表示されることがあります。
以下は、MyAppというプロジェクト名・ターゲット名のアプリの例です。

Terminalに表示される警告メッセージ

[!] The `MyApp [Debug]` target overrides the `ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES` build setting defined in `Pods/Target Support Files/Pods-MyApp/Pods-MyApp.debug.xcconfig'. This can lead to problems with the CocoaPods installation
    - Use the `$(inherited)` flag, or
    - Remove the build settings from the target.

[!] The `MyApp [Release]` target overrides the `ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES` build setting defined in `Pods/Target Support Files/Pods-MyApp/Pods-MyApp.release.xcconfig'. This can lead to problems with the CocoaPods installation
    - Use the `$(inherited)` flag, or
    - Remove the build settings from the target.

環境

  • Xcode: 11.5
  • Swift: 5
  • CocoaPods: 1.9.3

説明

対応方法のみ知りたい方は、読み飛ばしてください :ok_woman:

警告メッセージの日本語訳

MyApp [Debug/ Release] (最終的なアプリバンドル )のターゲットが、 Pods/Target Support Files/Pods-MyApp/Pods-MyApp.xxx.xcconfig で定義された、 ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES (Swift標準ライブラリ を常に埋め込む)というビルド設定を、オーバーライドしています。
これにより、CocoaPodsのインストールで問題が発生する可能性があります。」

つまり、CocoaPodsで管理しているビルド設定を、アプリのターゲットのビルド設定が上書きしちゃってるけどいいの?という注意喚起をしてくれています。

CocoaPodsで管理しているビルド設定
Screen Shot 2020-08-14 at 11.55.03.png

アプリのターゲットのビルド設定
Screen Shot 2020-08-14 at 11.57.07.png

ビルド設定「ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES 」とは?

ターゲットがSwiftコードを含まない場合でも、常にターゲットの製品にSwift標準ライブラリを埋め込むかどうかを設定します。YESに設定されている場合、アプリにSwift標準ライブラリを埋め込むようにXcodeに指示します。
例えば、

  • ターゲットがSwiftを含む他の製品を埋め込んでいる場合や、
  • Swiftを含まないがSwiftを含む製品をテストしているテストターゲットの場合

には、この設定を有効にすべきです。

Xcode Help 参照

Swift標準ライブラリとは?

Swift標準ライブラリは、Swiftプログラムを書くための機能のベースレイヤーを定義しています。
Int, Doubleなどの基本的なデータ型や、printのようなグローバル関数などが含まれています。
【Apple開発者ドキュメント】Swift Standard Library

ターゲットがSwiftコードを含んでいる場合に、アプリバンドルにコピーされます。
なお、製品がSwiftコードを含んでいるかどうかは、実行ファイル上でotool -Lを実行することで確認することができます。otool -Lの結果の中にSwiftライブラリが表示されていれば、製品はSwiftを使用していることになります。
【Apple開発者ドキュメント】Technical Q&A QA1881: Embedding Content with Swift in Objective-C

対応方法

対応方針

CocoaPods側で管理されている、「ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES」というビルド設定を、最終的なアプリバンドル のターゲットに引継ぎます。

1. Xcode上で変更する方法

メリット

変更方法が視覚的にわかりやすい。

デメリット

チームプロジェクトの場合、チームメイト全員が各自で pod install したあとに、確実にこの対応をするようにしなければならない。

方法

  1. 設定に移動
  2. ターゲットを選択
  3. 「Build Setting」を選択
  4. 「All」と「Combined」を選択
  5. "Always Embed Swift Standard Libraries" を検索
  6. 「$(inherited)」を値に入力、もしくは項目自体を削除。

Screen Shot 2020-08-14 at 21.02.40.png

Screen Shot 2020-08-11 at 22.01.22.png

2. Podfileに記述する方法

メリット

一度Podfileに記述すれば、自動的に pod install  したときに自動的に実行される。
個人差が発生しない。

デメリット

特になし?
人によっては見慣れないこと。

方法

以下の文をPodfileに追記した上で、 pod install を実行する。

Podfileに追加する文

post_install do |installer_representation|
    installer_representation.pods_project.targets.each do |target|
        target.build_configurations.each do |config|
            config.build_settings['ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES'] = '$(inherited)'
        end
    end
end

【stack overflow】What's ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES with CocoaPods, Swift 3 and Xcode 8
【blog】CocoaPods - Always Embed Swift Standard Libraries

おわりに

以上、CocoaPodsインストール時に発生する「ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES」の意味と対応方法2つについて、説明しました。
対応方法が書かれた記事はよく見かけるのですが、結局これってどういう意味なの?を説明した記事がなかなか見つからなかったので、両方をこの機会にきちんと調べてみました。
どなたかのお役に立てたら嬉しいです。

参考リンク

Xcode Help
【Apple開発者ドキュメント】Swift Standard Library
【Apple開発者ドキュメント】Technical Q&A QA1881: Embedding Content with Swift in Objective-C
【stack overflow】What's ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES with CocoaPods, Swift 3 and Xcode 8
【blog】CocoaPods - Always Embed Swift Standard Libraries

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

iOSのネットワーク接続状態の検出にはReachabilityよりシンプルなNWPathMonitorを使いましょう。

iOSでネットワーク接続状態の取得といえばReachabilityを使うといった印象が強くありませんか。

Reachabilityを使うには、まず
https://developer.apple.com/library/archive/samplecode/Reachability/Introduction/Intro.html
からApple の提供しているサンプルコードをダウンロードして・・・なんて手間がありましたが、もっと手軽に利用できるApple標準のクラスがありました。

簡単にネットワーク接続のモニタリングが可能

NWPathMonitor

ネットワークの接続状態やネットワークインターフェースなどの情報をモニタリングできるNetwork frameworkのクラス。iOS 12.0以降から利用可能。
https://developer.apple.com/documentation/network/nwpathmonitor

実装ステップ

利用箇所でまずフレームワークをインポートしておきましょう。

import Network

モニタリング用クラスのインスタンス化

解放されるとコールバックが受け取れないので、ViewControllerのプロパティなどとして保持しておきましょう。

let monitor = NWPathMonitor()

requiredInterfaceType引数を指定して、ネットワークインターフェース別にモニタリングすることも可能です。

let wifiMonitor = NWPathMonitor(requiredInterfaceType: .wifi)

サポートしているインターフェースのタイプは以下に記載があります。
https://developer.apple.com/documentation/network/nwinterface/interfacetype

ネットワーク接続状態の変更をハンドリングする

monitor.pathUpdateHandler = { path in
    if path.status == .satisfied {
        // Connected
    }
}

後述しますが、モニタリングは通常バックグラウンドキューで指定して実行されます。
クロージャー内でUIを操作する場合には、メインスレッドで行うようにしましょう。

ネットワーク接続状態のモニタリングを開始する

モニタリングイベントを配信するためのDispatchQueueを定義して、モニタリングを開始します。

private let queue = DispatchQueue(label: "com.sample")
monitor.start(queue: queue)

ソースコード全体

import UIKit
import Network

final class ViewController: UIViewController {

    private let monitor = NWPathMonitor()
    private let queue = DispatchQueue(label: "com.sample")

    override func viewDidLoad() {
        super.viewDidLoad()

        monitor.pathUpdateHandler = { path in
            if path.status == .satisfied {
                print("Connected")
            } else {
                print("Not Connected")
            }
        }

        monitor.start(queue: queue)
    }
}

まとめ

動画で載せたデモアプリの実装ですが、ViewController内を30行前後で書くことができました。
iOS12以降対応ですし、実用性もばっちりですね。
「ネットワーク接続状態の検出にはNWPathMonitor!」が定着すれば良いなと思います。

以上です :tada:
ありがとうございました。

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

R.Swift使い方 備忘録

画面遷移を簡潔なコードのみで実行してくれるライブラリ
R.Swiftの使い方について
自分用メモ

1、cocoapodからR.Swiftをインストール
2、R.generated.swiftが生成されていることを確認

注意

import R.Swift //は必要ない

3、遷移したい所にコードを記入

//aaaはMain.storyboard のMainの部分(全て小文字になる)
//bbbはViewControllerのstoryboardID(大文字も含む)
let vc = R.storyboard.aaa.bbb()!
self.present(vc, animated: true, completion: nil)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SwiftでFirebaseAppの「Use of unresolved identifier 'FirebaseApp'」が消えない....

はじめに

SwiftでFirebase使って、アプリを作っているのですが、最初問題なくビルドできていたのに、途中から、「Use of unresolved identifier 'FirebaseApp'」エラーが出始めて、エラーが表示されるけどビルドは通るのだが、何度もビルドしていると、そのうち、ビルドすら通らなくなったので、調べて解決できたので...

更新履歴

2020.8.14 初回投稿

環境

  • macOS Catalina(10.15.5)
  • Swift5.2.4
  • Xcode11.6

現象

1.PodsでFirebaseインストール後、アプリ開発途中ビルドすると「Use of unresolved identifier 'FirebaseApp'」エラー発生。しかし、ビルドは通る

2.何度かビルドを繰り返すと、ビルドが通らなくなる。

試したこと

1.↓を参考にpod updateなどを試みる
https://qiita.com/cusa/items/e0a7b6f2a87f99e00cd8
しかし、ある日これを試してもダメになってしまった。

2.さらに調べて、↓を参考に、import FirebaseCoreを追加してみる
https://tomolog.reafo.io/jp/article/error-firebaseapp-unresolvedidentifier-ga-kienaitoki

あっさりエラー消えた....

結論

ちゃんとFirebaseインストールしてうまくビルドできていたのに、ある日急に、「Use of unresolved identifier 'FirebaseApp'」のエラーが出たら、「import FirebaseCore」をApp Delegateに追加してみる。
というか、最初から追加しておく。

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

【Swift】Realm accessed from incorrect thread

題のエラーについて。

realmをクラス変数にして複数の関数でそのインスタンスを使いまわしていたとき、よくアクセスするスレッドがおかしいですよーなんてエラーが見られた。

どうもこのやり方は良くないらしく、データの操作を実行するたびに新しいrealmのインスタンスを作って読み書きなどしなければならないらしい。

個人的には何度もインスタンス化するのはパフォーマンス的にもコード的にも気持ちよくないのでは、?と感じたのだが、
取り敢えずパフォーマンス面においてはスレッドごとにrealmのインスタンスをキャッシュしてくれる様なのでそれを問題視する必要なんて無い様です。

BAD

RealmModel.swift
// bad bad bad bad bad bad bad bad
final class RealmModel {
    let realm = try! Realm()

    func saveBook(book: Book) {
        try! realm.write {
            realm.add(book)
        }
    }
    func updateBook(book: Book, title: String) {
        try! realm.write {
            book.title = title
        }
    }
}
// bad bad bad bad bad bad bad bad

OK

RealmModel.swift
// ok ok ok ok ok ok ok ok ok ok ok
final class RealmModel {
    func saveBook(book: Book) {
        let realm = try! Realm()
        try! realm.write {
            realm.add(book)
        }
    }
    func updateBook(book: Book, title: String) {
        let realm = try! Realm()
        try! realm.write {
            book.title = title
        }
    }
}
// ok ok ok ok ok ok ok ok ok ok ok

勉強になった。

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

【入門】iOS アプリ開発 #4【アーキテクチャの設計】

アーキテクチャの設計

今回はパックマンのゲームを構築するにあたり、全体のアーキテクチャを設計する。

仕様書から画面モードは4つあり、それぞれのモード内にはキャラクタがいて移動処理などがある。これらを1つ1つのオブジェクトとして管理し扱っていく。

Image2.png

設計方針

各画面モードをオブジェクトとして扱い、簡単に切り替え操作したい。

attractMode.startSequence()
// ・・・アトラクトモード実行中・・・

// アトラクトモードからクレジットモードへ切り替える
attractMode.stopSequence()
creditMode.startSequence()
// ・・・クレジットモード実行中・・・

こんな感じ。

またイベント・メッセージを画面モード内のオブジェクトにも簡単に通知したい。

attractMode.sendEvent(message: .Update, parameter: [16])
attractMode.sendEvent(message: .Touch, parameter: [x,y])

イベント・メッセージは表示更新タイミング(Update)やタッチイベントなどで、画面モードが無効のものには通知しないようにする。

まとめると、以下のようなオブジェクトの階層構造が作れるようにする。

Image4.png

これらの基本コンポーネントのクラスを作成する。

CbObject class(基底クラス)

/// Based object class
class CbObject {

    /// Kind of Message ID to handled events in object.
    enum EnMessage: Int {
        case None
        case Update
        case Timer
        case Touch
        case Swipe
        case Accel
    }

    /// When it is true, object can handle events.
    var enabled: Bool = true

    /// For accessing parent objects.
    private var parent: CbObject?

    /// Initialize self without parent object
    init() {
        parent = nil
    }

    /// Initialize self with parent object
    /// - Parameter object: Parent object
    init(binding object: CbObject) {
        parent = object
        parent?.bind(self)
    }

    /// Bind self to a specified object
    /// - Parameter object: Object to bind self.
    func bind( _ object: CbObject) {
        // TO DO: override
        // (This is pure virtual method.)
    }

    // Send event messages
    /// - Parameters:
    ///   - id: Message ID
    ///   - values: Parameters of message.
    func sendEvent(message id: EnMessage, parameter values: [Int]) {
        receiveEvent(sender: self, message: id, parameter: values)
    }

    /// Handler called by sendEvent method to receive events
    /// - Parameters:
    ///   - sender: Message sender
    ///   - id: Message ID
    ///   - values: Parameters of message
    func receiveEvent(sender: CbObject, message: EnMessage, parameter values: [Int]) {
        guard enabled else { return }
        if message == .Update {
            update(interval: values[0])
        } else {
            handleEvent(sender: sender, message: message, parameter: values)
        }
    }

    /// Event handler
    /// - Parameters:
    ///   - sender: Message sender
    ///   - id: Message ID
    ///   - values: Parameters of message
    func handleEvent(sender: CbObject, message: EnMessage, parameter values: [Int]) {
        // TO DO: override
        // (This is pure virtual method.)
    }

    /// Update handler
    /// - Parameter interval: Interval time(ms) to update.
    func update(interval: Int) {
        // TO DO: override
        // (This is pure virtual method.)
    }

}

基底クラスの CbObject は、init で上位のオブジェクトとの関連付けを行える。sendEventメソッドでメッセージを送信でき、メッセージが、.Update なら、updateハンドラが呼ばれ、それ以外は、handleEvent が呼ばれる。ハンドラは派生クラスでオーバーライドして扱う。プロパティの enabled を false にすると、ハンドラは呼ばれなくなる。

CbContainer class(派生コンテナ・クラス)

/// Container class that bind objects.
class CbContainer : CbObject {

    private var objects: [CbObject] = []

    /// Bind self to a specified object
    /// - Parameter object: Object to bind self.
    override func bind( _ object: CbObject) {
        objects.append(object)
    }

    /// Handler called by sendEvent method to receive events
    /// It sends messages to all contained object.
    /// - Parameters:
    ///   - sender: Message sender
    ///   - id: Message ID
    ///   - values: Parameters of message
    override func receiveEvent(sender: CbObject, message: EnMessage, parameter values: [Int]) {
        guard enabled else { return }

        super.receiveEvent(sender: sender, message: message, parameter: values)

        for t in objects {
            t.receiveEvent(sender: self, message: message, parameter: values)
        }
    }
}

派生コンテナ・クラスの CbContainer は、sendEventメソッドで受け取ったメッセージを、関連付けしている全てのオブジェクトに送信する。同様にプロパティの enabled を false にすると、イベント・ハンドラは呼ばれなくなる。

使用方法

class MyObject: CbObject {
    override func update(interval: Int) {
        print("Update!")
    }
}

func test() {
    root  = CbContainer()
    node1 = CbContainer(binding: root)
    node2 = MyObject(binding: node1)

    root.sendEvent(message: .Update, parameter: [16])
}

root に送ったメッセージが末端の node2 のオブジェクトに届くようになる。

まとめ

全体のオブジェクトの管理、イベント・メッセージの仕組みを設計した。

高々100行程度のコードだが、最初にこれらを決めておかないと、後々、苦労しそうな気がする。

次は、CbContainerクラスを継承して画面モード・クラスを作成していく。

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

UITextFieldの空文字""とnil

きっかけ

練習でサンプルアプリを作っていて、その中で
「TextFieldに何も文字が入力されていない状態でボタンを押した場合アラートを出す」という実装をしようと思いました。
今までTextFieldに何も入力されていないということを表現するために何となく空文字""を使っていましたが、ふと nilじゃいけないのか...??と疑問に思ったので色々試してみた結果、学んだことがあったので記しておきます。
その時に書いたコードが以下のものです。

ViewController
 @IBOutlet private weak var textField: UITextField!
 @IBOutlet private weak var label: UILabel!

 override func viewDidLoad() {
     super.viewDidLoad()
 }

 @IBAction func print(_ sender: Any) {
     guard textField.text != "" else {
     alert(message: "未入力です")
     return
     }
     label.text = textField.text
     view.endEditing(true)
 }

 private func alert(message: String) {
     let alertController = UIAlertController(title: "エラー",message: message, preferredStyle: .alert)
     let action = UIAlertAction(title: "OK", style: .default, handler: nil)
     alertController.addAction(action)
     present(alertController, animated: true, completion: nil)
 }

TextFieldに何も入力されていないままprintボタンを押すとこんな感じでアラートが出ます。
Simulator Screen Shot - iPhone 11 - 2020-08-14 at 00.56.20.png

疑問

要するに何も入力されてなければalertを表示させたいわけですが、

guard textField.text != ""

のところを

guard textField.text != nil

とやっちゃいかんのか?と思い、nilに変えて実行してみたところ、guard文をすり抜けてしまいalertが表示されませんでした。
textField.textの型はString?型でオプショナルなので、nilが入る余地があり、未入力ということは値が入っていないnilってことなのに何でguard文をすり抜けるんだろう?と疑問に思いました。

なぜなのか

まず、「なぜnilを入れた時にguard文をすり抜けてしまうのか」についてですが、これはTextFieldの初期値が""で設定されているからだそうで、そのため初期状態が""の状態でprintボタンを押しても「nilではない」と判定されてしまうため、guard文をすり抜けてしまうわけです。

えっ、じゃあTextField.textにnilって絶対入らなくない?と思いPlaygroundで色々試してみた結果、以下のようになりました。
スクリーンショット 2020-08-14 1.22.27.png
これを見る限りUITextFieldの初期値は空文字""で、後からnilを代入しても空文字""扱いとなるそうです。
ついでにUILabelでも試してみたところ、こちらはnilを代入したところちゃんとnilが入ってました(当たり前)。

じゃあTextFieldにnilが入らないってことはアンラップしないで使っていいの?

TextFieldにnilが入る場合が存在しないってことはわざわざアンラップしなくてもいいのか?
(現に僕もナチュラルにguard textField.text != "" elseとアンラップしないで書いていた)
と思い現役のエンジニアさんに質問させていただいたところ、
「型がOptionalなら、将来的に挙動が変わってnilが返ってくることもありえるので、現在挙動としてnilがありえなかったとしても、型がOptionalの場合はnilを考慮しないといけない」とのことでした。
ちゃんとしたやり方?で書くとなると以下のように書くのが良いそうです。

guard !(textField.text ?? "").isEmpty else { ... } 

やはりオプショナル型はしっかりアンラップを行い、String?型をString型にした上でisEmptyプロパティにアクセスし空欄かどうかチェックする、というのが定石だそうです。

まとめ

オプショナル型を適当に扱いがちっていうのは初心者にあるあるだと思うので、今回の気付きはとても勉強になりました。
「必ず将来長期に渡ってその値はnilになり得ないのか」ということをしっかり意識してコードを書いていきたいです(小並感)。

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

【Swift】正規表現で含有判定

文字列を含んでいるのか

  • 正規表現パターン:patternを引数で渡し、含んでいるのか評価した結果をtrue/falseで返します。
  • エラーがthrowされた場合、正規表現が間違えています
extension String {
    func contain(pattern: String) -> Bool {
        guard let regex = try? NSRegularExpression(pattern: pattern, options: NSRegularExpression.Options()) else {
            return false
        }
        return regex.firstMatch(in: self, options: NSRegularExpression.MatchingOptions(), range: NSMakeRange(0, self.count)) != nil
    }
}

利用方法

実装

print("apple".contain(pattern: "a.*e"))
print("apple".contain(pattern: String()))
print("".contain(pattern: String()))

出力

true
false
false

参考

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