20210114のSwiftに関する記事は6件です。

【Swift】動的な画面でのパフォーマンスを考慮したUIImageViewの使用

はじめに

動的な画面ではUIImageViewを高速に使用する場合、CALayerの使用方法によっては処理が重くなってしまう場合があります。
コメント欄などでのパフォーマンスを考慮した角丸やBorderを使用したUIImageViewの使用について書きます。

一般的な実装方法

一般的にフチのある丸画像を使用する際は

ViewController.swift
imageView.layer.cornerRadius = imageView.frame.height / 2
imageView.layer.borderWidth = 4
imageView.layer.borderColor = UIColor.orange.cgColor

このようにCALayerにたいして指定し、実装するかと思います。

問題点

しかしCALayerに指定して実装した場合は、コンテンツをスクロールしたとき、毎フレームで画像処理のためのオフスクリーンレンダリングが走ります。
画像の変更がない場合や関係ない画像に関係ないViewの動きに対してもオフスクリーンレンダリング発生してしまう場合があります。
そのためCPUへの負荷が非常に高くなってしまいます。
また、画像が一瞬四角に表示されるなど、意図せぬ挙動をしてしまう場合があります。
よってLayerのcornerRadiusは、高速な処理をする画面での使用には向きません。

そのため

  • 角丸などの加工済みの画像を用意する
  • ライブラリなどで使用前に画像を加工する
  • 角丸のViewを上から重ねる などで対策する必要があります。

実装方法については// TODOということでまた書きます()

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

【Swift】アクセス修飾子を理解して使い分ける

はじめに

Swiftでコーディングする際、アクセス修飾子を適当に使っちゃってないですか?
雰囲気で使いわけてレビューで突っ込まれてないですか?

なぜ定数(変数)を使うのか、なぜその修飾子を使うのか理解して使うために自分なりにまとめたので、よかったら参考にしてください!

変数と定数

let(定数)
値を変更する可能性がない場合に使う
static let
全インスタンスから共通で利用できる
var(変数)
後から値を変更することができる

let hoge = hogehoge

static let hoge = hogehoge

var hoge = hogehoge

アクセス修飾子

Set
入れた値を使って他のプロパティの値に渡すことができる
Get
他のプロパティの値を受け取るのことができる

追記: getterは同一モジュール内、setterは同一スコープ内からアクセスできる
(@takehito-koshimizu さんありがとうございます🙏)

private(set) var hoge = hogehoge

Public
別モジュールから呼び出せるが継承やオーバーライドが不可能
Private
同スコープ内からのみ呼び出せる
fileprivate
同じfile内であれば呼び出せる

private let hoge = hogehoge

private var hoge = hogehoge
public let hoge = hogehoge

public var hoge = hogehoge
fileprivate let hoge = hogehoge

fileprivate var hoge = hogehoge

参考になりましたでしょうか?よければLGTMくれると僕がめっちゃ喜びます!

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

Swift UITextViewのplaceholder

UITextViewにplaceholderがない!?

twitter風アプリを作ろうと思った時にtextfieldと同じようにtextView.placeholder = ""って感じでコードを書くと
Value of type 'UITextView' has no member 'placeholder'と怒られてしまいました。
え、textViewってplaceholderないの...
ということで、なんとかしてtextViewplaceholderを実装していきましょう。

今回はUITextViewを継承したTextViewというclassを作成してそのTextViewの中にUILabeladdSubViewしていきます。

TextView.swift
class TextView: UITextView {

    public let placeholderLabel: UILabel = {
        let label = UILabel()
        label.textColor = .lightGray
        label.font = .systemFont(ofSize: 18)
        label.numberOfLines = 0
        return label
    }()

    override init(frame: CGRect, textContainer: NSTextContainer?) {
        super.init(frame: frame, textContainer: textContainer)
        addSubview(placeholderLabel)

        placeholderLabel.translatesAutoresizingMaskIntoConstraints = false
        let cons = [
            placeholderLabel.topAnchor.constraint(equalTo: topAnchor, constant: 8),
            placeholderLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 5),
            placeholderLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -5),
        ]

        NSLayoutConstraint.activate(cons)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

}

こんな感じでTextViewを定義しました。

あとはViewControllertextViewを表示するだけですね!

ViewController.swift
class ViewController: UIViewController {

    let textView: TextView = {
        let textView = TextView()
        textView.placeholderLabel.text = "placeholderだよー"
        textView.font = .systemFont(ofSize: 18)
        return textView
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(textView)
        textView.frame = .init(x: 0, y: 100, width: view.frame.size.width, height: view.frame.size.height-200)
    }
}

これでいい感じに表示されました!
でも、このままだと入力した文字とplaceholderLabelの文字が重なってしまいますね。
んー、どうしよっかと考えた結果UITextViewDelegatetextViewDidChange(_ textView: UITextView)を使うことにしました。

ViewController.swift
extension ViewController: UITextViewDelegate {
    func textViewDidChange(_ textView: UITextView) {
        if textView.text.isEmpty {
            self.textView.placeholderLabel.alpha = 1
        } else {
            self.textView.placeholderLabel.alpha = 0
        }
    }
}

こんな感じです。
textView.textが空だったらplaceholderLabelを表示し、そうでなかったらplaceholderLabelを非表示にする。

まとめ

textViewにplacehoderを実装するだけで結構コード書かないといけないですね笑
もっと簡単に実装できる方法があればどなたか教えてください!

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

【Swift】アプリとFirebaseの連携方法

はじめに

アプリを作成するにあたり、ログインの機能はよく使用します。
AppleIDを使ったログインやTwitter、Facebookを使ったログインの方法があります。

今回は、それらの実装の簡略化やログイン後のアカウント管理を
行うことができるFirebaseと、アプリを連携するまでの手順を記載していきます。

実際のログインの実装は別記事で紹介しています。
FirebaseUIを使ってTwitterログイン機能を実装する

環境
・Swift version 5.3
・XCode version 12.3
CocoaPods version 1.10.1

実装方法

Qiitaでは一ヶ月に投稿できる画像の容量が決まっているらしいので、
なるべく節約した最小限の画像にしたいと思います。申し訳ございません・・・。

アプリ作成

ログイン機能を実装するアプリを作成します。
今回は、SampleAppleLoginというプロジェクト名で作成しました。

画像のBundle Identifierをコピーしておいてください。
スクリーンショット 2021-01-14 16.28.22.jpg

Firebase 新規プロジェクト作成

Firebaseで新規プロジェクトを作成します。

Firebase consoleにアクセスしたら、+ プロジェクトを追加をクリック
-> プロジェクト名を指定して「続行」をクリック
-> このプロジェクトで Google アナリティクスを有効にするは無効にしてプロジェクトを作成
-> プロジェクトを作成できました。と表示されたら「続行」をクリック

Firebaseとアプリの連携

プロジェクトの概要からiOSのボタンをクリックしてアプリを追加します。
スクリーンショット 2021-01-14 16.39.37.jpg

①アプリの登録にてiOS バンドル IDを入力するフィールドがあるので、
こちらに先ほど作成したアプリのBundle Identifierをコピペしてください。

その他の入力は任意なので「次へ」をクリックします。

GoogleService-Info.plistをクリックしてダウンロードして「次へ」をクリック
スクリーンショット 2021-01-14 16.43.12.jpg

以降の項目の設定は後ほど行うので、
「次へ」 -> 「次へ」 -> 「コンソールへ進む」と進んでください。

これでFirebase側にアプリの登録をすることができたので、
アプリ側でFirebaseと連携するための処理を行います。

画面右上の虫眼鏡マークをクリックしてください。(もしくはcommand + スペースキー)
スクリーンショット 2021-01-14 16.47.05.jpg
検索欄が出てくるので「ターミナル」と入力Enterを押します。

まず、ディレクトリの移動を行います。

cdコマンドでディレクトリを移動するのですが、
移動先は先ほど作成したアプリのPATHを入力してください。
私の場合は下記のようなコマンドになります。

よくわからない方は、作成したアプリのフォルダをドラッグ&ドロップしてください。

$ cd Desktop/project/SampleAppleLogin 

次にPodfileの作成を行います。

$ pod init

念のためPodfileが作成されていることを確認します。
私の場合は、Podfileが作成され下記の状態になりました。

$ ls
Podfile             SampleAppleLogin        SampleAppleLogin.xcodeproj

Podfileの編集を行います。

$ vi Podfile

インストール対象を追記します。
追記するものは下記の2つになります。

pod 'FirebaseUI'
pod 'Firebase'

まず編集モードにする必要があるので、iを押します。
すると左下に-- INSERT --と表紙されると思います。

その状態ですと文字を入力することができます。

追記すると次のようになると思います。
(バージョンによって記載内容は多少異なります。)

# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'SampleAppleLogin' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!

  # Pods for SampleAppleLogin
  pod 'FirebaseUI'   // 追加
  pod 'Firebase'   // 追加

end

追記後にファイルを保存する必要があるので、
escを押し編集モードを解除後、:wqを順に押し保存します。

その後、インストールを行います。

$ pod install

エラーが発生しなければOKです!

発生した場合はエラー内容でググってみてください。
(基本的にエラーは出ないはずです。)

Xcodeを一度閉じて、Finderからアプリを開くのですが、
この時プロジェクト名.xcworkspaceのファイルを開いてください。

次に、Firebaseでプロジェクトを作成する際にダウンロードした、
GoogleService-Info.plistをアプリ内に置きます。

ViewController.swiftなどと同じ階層に置きます。
スクリーンショット 2021-01-14 17.11.29.jpg

GoogleService-Info.plistクリックすると真ん中の画面が変わると思います。
その中の、REVERSED_CLIENT_IDの行のvalueの列の値をコピーしてください。

com.googleuser 〜といった内容だと思います。

一番上の階層のプロジェクト名をクリックすると真ん中の画面が変わると思います。
スクリーンショット 2021-01-14 17.17.13.jpg
タグの中からInfoを選択してください。
一番下のURl Typesを開き、+を押して追加します。

右上のURL Schemesに先ほどコピーしたものを貼り付けます。

次にAppDelegate.swiftを開き、下記のように編集します。
※ application( )メソッドは複数あるので間違えないようにしてください。

AppDelegate.swift
import Firebase    // 追加

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // Override point for customization after application launch.
    FirebaseApp.configure()    // 追加
    return true
}

さいごに

本記事は、FirebaseUIでログイン機能を実装する予定でしたので、
CocoaPodsでFirebaseUIをインストールしました。

別の方法でログインする場合はインストールする必要がない可能性があるので、
その方法にしたがってください。

また、ここまででは連携までしか終わっていないので、
実際のログイン機能の実装を行う必要があります。

そちらに関しての記事は下記になります。
FirebaseUIを使ってTwitterログイン機能を実装する

以上、最後までご覧いただきありがとうございました。

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

swiftのErrorとNSErrorの違いがよく分からないので自分なりにまとめてみた

はじめに

いつも、swiftで開発しているのですが、Error・NSErrorの2つのエラーオブジェクトが出てきて
いつも何で分けられてるの?と疑問に思いながらErrorからNSErrorに型キャストしたりしてました。

よく分からないままだと流石にまずいなと思い、Qiitaに備忘録として残そうと思った感じです。

ErrorとNSErrorの違いとは

そもそも、この2つの違いは何でしょうか?

調べてみると

・NSErrorはObjective-Cのエラーオブジェクトでクラスとして定義されている。

・Errorはswiftのエラーオブジェクトでプロトコルとして定義されている。

という感じでした。

更に、深く調べてみましょう。

NSError

NSErrorObjective-Cで使われていたエラーオブジェクトでエラー状態に関する情報が含まれています。

どんな情報が含まれているのかというと
domaincodeuserInfoという3つの情報が含まれています。

分かりやすようにまとめてみました↓

情報 意味
domain エラーの種類を識別するための文字列
code エラーの種類を識別するための整数値
userInfo エラーに関する付加情報

swiftのErrorはNSErrorに常にキャストできるようになっており、それらの情報を参照することができます。

実際にコードを書いてみるとこんな感じです↓

    func manager(error: Error) {
        // ErrorからNSErrorにキャスト
        let nsError = error as NSError
        // 欲しい情報を参照する
        print(nsError.domain)
        print(nsError.code)
        print(nsError.userInfo)
    }

例えば何かのマネージャークラスのデリゲートメソッドがあって
エラーが発生したら呼び出されるメソッドがあるとします。

そしたら、そのエラーをNSErrorにキャストして欲しい情報を参照していくという流れですね。

NSErrorの使いどころとは

swiftではObjective-Cとの互換性を保つために存在していますが、swiftのエラーハンドリングが強力なのもあって、NSErrorで使っていたdomainやcodeを使う機会は減ってしまったそうです。

ただ、2つの言語が混在しているプロジェクトであればNSErrorを意識する場面に直面するそうですね。

私は、全くObjective-Cを触ったことがないので分かりませんが自分の開発しているアプリでは
domainやcodeの情報が欲しい時があるので割とキャストして使っています。

Error

最初の方で述べたようにswiftで使われているエラーオブジェクトでプロトコルとして定義されています。

そんなswiftのError準拠した型がエラーを表現する型として扱えることを示すためのプロトコルであって
準拠するために必要な実装はないです。

つまり、swiftのErrorはエラー情報を表現するプロトコル(規約)なんですね。

因みに余談ですが、swift2ではErrorはErrorTypeという名前だったそうで
swift3からErrorという名前に変わったそうです。

Errorの使いどころとは

このErrorプロトコルに準拠する型は、列挙型(enum)に準拠するのが一般的で
発生するエラーをまとめて記述できるというメリットがあるからです。

更に、プログラム全体で起こり得るあらゆるエラーを一つの列挙型で定義せずに
エラーの種類によって別の型を定義するのが常だそうですね。

では実際に、Errorプロトコルを準拠して列挙型で定義してみましょう。

今回は、API通信を行う時に発生するであろうエラーを列挙型で定義してみます。

enum ApiError: Error {
    case networkError
    case decodeFailed
    case responceFailed
    case unknown
}

このように定義することができます。

後は引数を持たせて、このような使い方が出来ます。
以下の例では、computedプロパティを使って、ケースごとにメッセージを返すようにしています↓

enum ApiError: Error {
    case networkError
    case decodeFailed(Error)
    case responceFailed(Error?)
    case unknown

    var message: String {
        switch self {
        case .networkError:
            return "通信エラーが発生しました"

        case .decodeFailed(let error):
            return "デコードに失敗しました \(error.localizedDescription)"

        case .responceFailed(let error):
            return "レスポンスの取得に失敗しました \(error?.localizedDescription ?? "error")"

        case .unknown:
            return "不明なエラーが発生しました"
        }
    }
}

let networkErrorMessage = ApiError.networkError.message
let decodeErrorMessage = ApiError.decodeFailed(error).message

Errorからエラーメッセージを取得したい場合は、localizedDescriptionプロパティを使う方法があります。

このプロパティは元々、NSErrorが定義しているプロパティなので、swiftのErrorが持っているプロパティではないのですがFoundationフレームワークをリンクしていると使えるようになります。

なぜ使えるのかというと、そもそもFoundationフレームワークはObjective-Cで作られており、Objective-CではFoundationフレームワークに含まれるNSErrorクラスを使ってエラーハンドリングしていたからです。

なので、swiftでもFoundationをインポートすればNSErrorクラスが使えるということです。

他にもInt型String型なども引数に持たせることが可能で
エラーに付随する情報を表現することが出来ます。

enum DatabaseError {
    case networkError
    case invalidEntry(reson: String)
}

let invalidEntryReson = DatabaseError.invalidEntry(reson: "~が理由で無効です")

おわり

ErrorとNSErrorの違いをまとめてみました。
swiftしか触ったことがないので、かなりswift寄りの記事になってしまいましたね。

もし、間違っている部分があればコメントして下さると有り難いです。

最後に下記にて参考資料を載せておきます。

参考資料

Swift3時代のErrorとNSErrorに関するいくつかの実験
Swift 4.0 エラー処理入門
[Swift] Swiftのエラー処理についてざっくりとまとめてみた

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

[コピペ用] Resultのためのテスト関数群 [XCTest]

僕のためのコピペ用です。

func M_XCTAssertResultIsSuccess<T, E>(_ result: @autoclosure () -> Result<T, E>, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line) {
    switch result() {
    case .success: ()
    case .failure:
        XCTFail("M_XCTAssertResultIsSuccess faild: Reuslt is not Success.", file: file, line: line)
    }
}

func M_XCTAssertResultIsFailure<T, E>(_ result: @autoclosure () -> Result<T, E>, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line) {
    switch result() {
    case .failure: ()
    case .success:
        XCTFail("M_XCTAssertResultIsFailure faild: Reuslt is not Failure.", file: file, line: line)
    }
}

///
func M_XCTAssertResultEqualValue<T: Equatable, E>(_ result: @autoclosure () -> Result<T, E>, _ value: @autoclosure () -> T, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line) {
    switch result() {
    case let .success(r):
        if r != value() {
            XCTFail("M_XCTAssertResultEqualValue faild: \(r) is not equal to \(value()).", file: file, line: line)
        }
    case .failure:
        XCTFail("M_XCTAssertResultEqualValue faild: result is Failure.", file: file, line: line)
    }
}

func M_XCTAssertResultEqualError<T, E: Equatable>(_ result: @autoclosure () -> Result<T, E>, _ value: @autoclosure () -> E, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line) {
    switch result() {
    case let .failure(e):
        if e != value() {
            XCTFail("M_XCTAssertResultEqualError faild: \(e) is not equal to \(value()).", file: file, line: line)
        }
    case .success:
        XCTFail("M_XCTAssertResultEqualError faild: result is Success.", file: file, line: line)
    }
}

///
func M_XCTAssertResultGreaterThan<T: Comparable, E>(_ result: @autoclosure () -> Result<T, E>, _ value: @autoclosure () -> T, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line) {
    switch result() {
    case let .success(r):
        if r <= value() {
            XCTFail("M_XCTAssertResultGreaterThan faild: \(r) is not greater than \(value()).", file: file, line: line)
        }
    case .failure:
        XCTFail("M_XCTAssertResultGreaterThan faild: result is Failure.", file: file, line: line)
    }
}

func M_XCTAssertResultGreaterThanOrEqual<T: Comparable, E>(_ result: @autoclosure () -> Result<T, E>, _ value: @autoclosure () -> T, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line) {
    switch result() {
    case let .success(r):
        if r < value() {
            XCTFail("M_XCTAssertResultGreaterThanOrEqual faild: \(r) is not greater than or equal to \(value()).", file: file, line: line)
        }
    case .failure:
        XCTFail("M_XCTAssertResultGreaterThanOrEqual faild: result is Failure.", file: file, line: line)
    }
}

func M_XCTAssertResultLessThan<T: Comparable, E>(_ result: @autoclosure () -> Result<T, E>, _ value: @autoclosure () -> T, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line) {
    switch result() {
    case let .success(r):
        if r >= value() {
            XCTFail("M_XCTAssertResultLessThan faild: \(r) is not less than \(value()).", file: file, line: line)
        }
    case .failure:
        XCTFail("M_XCTAssertResultLessThan faild: result is Failure.", file: file, line: line)
    }
}

func M_XCTAssertResultLessThanOrEqual<T: Comparable, E>(_ result: @autoclosure () -> Result<T, E>, _ value: @autoclosure () -> T, _ message: @autoclosure () -> String = "", file: StaticString = #filePath, line: UInt = #line) {
    switch result() {
    case let .success(r):
        if r > value() {
            XCTFail("M_XCTAssertResultLessThanOrEqual faild: \(r) is not less than or equal to \(value()).", file: file, line: line)
        }
    case .failure:
        XCTFail("M_XCTAssertResultLessThanOrEqual faild: result is Failure.", file: file, line: line)
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む