20191004のSwiftに関する記事は7件です。

@dynamicMemberLookupを使ってSwiftにKotolinのapplyを実装する

私以外にもKotolinのapplyをSwiftでもやりたいと思ったことのある人はいるでしょう。
例えば以下のようにUILabelを定義する場面を考えます。

let label: UILabel = {
   let it = UILabel()
   it.text = "label.." 
   return it
}()

以前までは、以下のようなプロトコルを定義することで

protocol Applicatable {}

extension Applicatable {
  func apply(_ closure: ((Self) -> ())) -> Self {
        closure(self)
        return self
    }
}

extension NSObject: Applicatable {}

それっぽく実装することができました。

let label = UILabel().apply { it in
   it.text = "label.."
}

しかし言語の進化により、もっとKotolinのapplyに近づけることができるようになりました。
つい先日DuctTapeというライブラリが公開されたのをきっかけにそれが可能になってたことを知りました。(Swiftのキャッチアップに遅れぎみです)

dynamicMemberLookup

Swift4.2で追加された機能ですが

@dynamicMemberLookup
struct StringMaker {
    subscript(dynamicMember value: String) -> String {
        return value
    }
}

let maker = StringMaker()
debugPrint(maker.id) // String: id
debugPrint(maker.name) // String: name

Swift5.1からKeyPathを使えるようになったみたいです。

@dynamicMemberLookup attribute requires ‘xxx’ to have a ‘subscript(dynamicMember:)’ method that accepts either ‘ExpressibleByStringLiteral’ or a keypath

これを使って参考ライブラリをパクりつつインターフェースの変更を40行程度実装すると

      let label = UILabel().apply { $0
            .text("Apply Swift")
            .textAlignment(.center)
            .textColor(.white)
            .backgroundColor(.black)
        }

debugPrint(label.text) // Optional(Apply Swift)

上記のような実装をすることができました。
一応ソースはGistに公開しておきます。

https://gist.github.com/churabou/7243d4ac9121a56513bb16023e3698f7


デモ

final class ViewController: UIViewController {
    override func loadView() {
        view = UILabel().apply { $0
            .text("Apply Swift")
            .textAlignment(.center)
            .textColor(.white)
            .backgroundColor(.black)
        }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SkeletonView

よく商用アプリでも使われているのを見るローディングライブラリである「SkeletonView」を使用したので、使い方の備忘録として書き残しておきます。

import SkeletonView
    override func viewDidLoad() {
        super.viewDidLoad()
        // TableViewの行の高さを可変にする
        tableView.rowHeight = UITableView.automaticDimension
        // UITableView.automaticDimensionを設定している場合に、SkeletonViewを使うなら
        // estimatedRowHeightの設定が必要
        tableView.estimatedRowHeight = 140
        // スケルトン表示開始
        view.showAnimatedGradientSkeleton()
    }
            defer {
                // スケルトン表示終了
                self.view.hideSkeleton()
            }
// MARK: - SkeletonTableViewDataSource
extension ViewController: SkeletonTableViewDataSource {
    /// スケルトン表示時のセクション数
    func numSections(in collectionSkeletonView: UITableView) -> Int {
        return 2
    }
    /// スケルトン表示時の行数
    func collectionSkeletonView(_ skeletonView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 2
    }
    /// スケルトン表示する再利用セルのIdentifier
    func collectionSkeletonView(_ skeletonView: UITableView, cellIdentifierForRowAt indexPath: IndexPath) -> ReusableCellIdentifier {
        switch indexPath.row {
        case 0:
            return "summaryCell"

        case 1...:
            return "detailCell"

        default:
            DispatchQueue.main.async {
                self.showAlert(title: "予期せぬエラー", message: "予期しないセルがあります")
            }
            return ""
        }
    }
}

参考

https://github.com/Juanpe/SkeletonView

https://dev.classmethod.jp/smartphone/iphone/oss-skeleton-view-introduction/

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

新しいバージョンが利用可能になったことをユーザにお知らせする - Siren

Siren とは?

アプリの新しいバージョンが利用可能になったことをユーザにお知らせアラートを表示するライブラリです。
本内容は Siren 5.2.1 をもとに記述しています。

skip

シンプルな実装

import Siren 後、Siren.shared.wail() を didFinishLaunchingWithOptions で呼び出すだけで動作します。

AppDelegate.swift
import Siren // Line 1
import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        window?.makeKeyAndVisible()
        Siren.shared.wail() // Line 2
        return true
    }
}

カスタマイズ

Siren の Manager を設定することで次のようなカスタマイズが可能です。

  • JP App Store でバージョンチェックしたい(デフォルトではUS)
  • バージョン(メジャー、マイナー、パッチ、リビジョン)毎に細かく表示を分けたい
  • アラートの表示頻度を変更したい(起動毎、日に1回、週に1回)
  • ユーザの選択肢を制限したい(強制アップデート、次回更新、このバージョンをスキップ)

JP App Store でバージョンチェックしたい

デフォルトでは US App Store に対してバージョンチェックを行います。
日本の App Store に対してバージョンチェックを行いたい場合は日本の App Store 向けに初期化したAPIManagerを用意します。

let siren = Siren.shared
siren.apiManager = APIManager.init(countryCode: "JP")

バージョン(メジャー、マイナー、パッチ、リビジョン)毎に細かく表示を分けたい

RulesManaer を設定してアラートを表示するルールをカスタマイズ出来ます。
メジャー、マイナー、パッチ、リビジョンなど全ての更新に個別のルールを設定出来ます。

// 起動ごとにアラート表示、強制アップデートのルール
let forceRules = Rules.init(promptFrequency: .immediately, forAlertType: .force)

// 1日1回アラート表示、アップデートのタイミングはユーザが選べるルール
let optionRules = Rules.init(promptFrequency: .daily, forAlertType: .option)

// メジャーバージョンが上がった場合は強制アップデート
// マイナーバージョン以下のアップデートの場合はユーザに選択可能とする
let siren = Siren.shared
siren.rulesManager = RulesManager(
    majorUpdateRules: forceRules,
    minorUpdateRules: optionRules,
    patchUpdateRules: optionRules,
    revisionUpdateRules: optionRules,
    showAlertAfterCurrentVersionHasBeenReleasedForDays: 1)
)

Rules

利用頻度が高そうなルールがあらかじめ用意されています。

Rule 説明
annoying アプリを起動するたびに表示
アプリの更新をスキップ可能
critical アプリを起動するたびに表示
アプリの更新を強制
default 日に1回表示
アプリの更新をスキップ可能
このバージョンをスキップも可能
hinting 週に1回表示
アプリの更新をスキップ可能
persistent 日に1回表示
アプリの更新をスキップ可能
relaxed 週に1回表示
アプリの更新をスキップ可能
このバージョンをスキップも可能
let siren = Siren.shared
siren.rulesManager = RulesManager(
    majorUpdateRules: .critical,
    minorUpdateRules: .default,
    patchUpdateRules: .default,
    revisionUpdateRules: .default,
    showAlertAfterCurrentVersionHasBeenReleasedForDays: 1)
)

独自 Rules

バージョンチェックの頻度やタイミング、ユーザへの選択肢を独自に設定することも可能です。
独自ルールで設定できる内容は下記の2つです。

promptFrequency

ユーザにアプリ更新を促す頻度です。

パラメタ 説明
immediately アプリを起動するたびに表示
daily 日に1回表示
weekly 週に1回表示

forAlertType

表示するアラートタイプです。ユーザが選べるボタンの数が変わります。

パラメタ 説明 アラート
force ユーザにアプリの更新を強制する force
option 今すぐアプリを更新するか、起動時に更新する
どちらかユーザが選択できる
option
skip ・今すぐアプリを更新する
・次回の起動時に更新する
・このバージョンはスキップする
の内からいずれかをユーザが選択できる
skip

releasedForDays

デフォルトでは更新アラートの表示を1日遅らせているそうです。
(showAlertAfterCurrentVersionHasBeenReleasedForDays: 1)
これは全ての地域の App Store CDN でバイナリが利用可能になる時間を待つためです。
通常、6〜24時間かかるため1日遅らせているようです。
showAlertAfterCurrentVersionHasBeenReleasedForDays に 0 を指定すると即時チェックとなりますがアラートが表示されるのに App Store にアップデートがまだ存在しない状況が発生する可能性があります。

Localization

デフォルトではアプリの言語設定に従ってメッセージが表示されます。
設定が存在しない場合は英語で表示されます。
SirenTest_xcodeproj.png
「設定 > 一般 > 言語と地域 > 使用言語」でメッセージが切り替わります。
Simulator Screen Shot - iPhone Xs - 2019-09-20 at 15.25.14.png Simulator Screen Shot - iPhone Xs - 2019-09-20 at 15.22.04.png

言語を強制したい場合

例えばどんな場合でも日本語で表示したい場合は PresentationManager を指定します。

siren.presentationManager = PresentationManager(forceLanguageLocalization: .japanese)

独自の文言で表示したい場合

あらかじめ用意されたメッセージではなくオリジナルな文言を用いたい場合も PresentationManager を設定します。
ボタンの文字色も変更できます。

        siren.presentationManager = PresentationManager(
            alertTintColor: UIColor.orange,
            appName: "アプリ名",
            alertTitle: "新しいバージョンがあります",
            alertMessage: "新バージョンではXXX機能がご利用頂けます",
            updateButtonTitle: "アップデートする",
            nextTimeButtonTitle: "今はしない",
            skipButtonTitle: "次のバージョンまで表示しない",
            forceLanguageLocalization: .japanese)

Simulator Screen Shot - iPhone Xs - 2019-09-20 at 15.50.03.png

オリジナルのメッセージには新しいバージョン番号が表示されていますが、独自の文言を設定する場合には新しいバージョン番号を差し込むことは今のところできません。

iOS13 対応

5.2.1 より古いバージョンでは、iOS13でダイアログが表示されてすぐ画面遷移してしまう不具合が発生していますので 5.2.1 以降へアップデートしましょう。

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

ObjectMapper failed to serialize responseが出たらJSONを疑え

はじめに

APIでJSONを取得してくる時に、静かにエラーになっていた。
ニュースアプリを作っているのですが、記事の筆者の情報が取れていなかった。

環境

pod 'AlamofireObjectMapper', '~> 5.2'
pod 'ObjectMapper', '~> 3.4'
Swift4.2
Xcode11
target? iOS:12.1

エラーと対処法

NetworkUtils.swift
let requestReference = Alamofire.request(url, method: .get).responseArray(keyPath: keyPath) { 
  (response: DataResponse<[T]>) -> Void in
      if let result = response.result.value {
            observer.onNext(
            observer.onCompleted()
       } else if let error = response.result.error {
            (こっちに入っていた)
       }

errorを見てみると、

ObjectMapper failed to serialize response
とある。

調べてみるとJSONの内容が怪しいとのこと。

JSONが正しいか判断してくれるサイト↓
https://jsonlint.com/

ここで、無駄な改行を消したらソースは変更せずに解決しました!

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

AppleWatchをブルブルさせようとしたら少しハマった

https://qiita.com/koogawa/items/3dcc72e77ce9e5b106b1
この記事を参考に,ブルブルさせようとしたが少しハマった話

以下をとりあえずコピペするが、もちろんバージョンアップされまくってるのでエラーが出る

WKInterfaceDevice.currentDevice().playHaptic(type: WKHapticType)

currentDevicecurrunt
playHapticplayにrenameされているらしい

とりあえず,これで実行してみる

WKInterfaceDevice.current().play(WKHapticType.notification)

が,実行されないしなぜかエラーも出ない

WKInterfaceDevice.current().play(.notification)

これで動きました

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

FirebaseAnalytics イベント名のバリデーション

FirebaseAnalyticsのイベント名は使用できる文字に制約があるが、無効な文字列が使われてても気づかないので、Unit Testで検知するようにした。
FirebaseAnalyiticsの導入に関しては記載しない。

  • Xcode11.1
  • Swift5

イベント名はenumでどこかに纏めて記述しておく。
送信箇所が多いと結構長いリストになる。

enum EventName: String, CaseIterable {
    case tapHoge = "tap_hoge"
    case tapFuga = "tap_fuga"
    ...
}

:books: バリデーション

制約はframework内の FIRAnalytics に以下の記述がある。

Should contain 1 to 40 alphanumeric characters or underscores.
The name must start with an alphabetic character.
Some event names are reserved. See FIREventNames.h for the list of reserved event names.
The "firebase_", "google_", and "ga_" prefixes are reserved and should not be used.
  • 1 ~ 40文字の英数字か_(アンダースコア)で構成されていないとならない
  • アルファベットで始まらなければならない
  • いくつかのイベント名は FIREventNames.h で定義されている
    • 被っても問題なさそうなのでスルー
  • "firebase_", "google_", "ga_" から始まってはいけない

:green_heart: テストコード

上記条件をそのままテストコードにした。

func testLogEventNames() {
    let analyticsReservedPrefix = ["firebase_", "google_", "ga_"]
    EventName.allCases.forEach { (name) in
        // !length.isEmpty && length <= 40
        XCTAssertTrue(!name.rawValue.isEmpty && name.rawValue.count <= 40, "Length error: \(name.rawValue)")
        // alphanumerics or "_"
        XCTAssertTrue(name.rawValue.isAlphanumericOrUnderscore, "String error: \(name.rawValue)")
        // has prefix alphabetic
        XCTAssertTrue(name.rawValue.hasPrefixLetter, "Prefix error: \(name.rawValue)")
        // not has prefix "firebase_", "google_", and "ga_"
        XCTAssertTrue(analyticsReservedPrefix.filter({ name.rawValue.hasPrefix($0) }).isEmpty, "Prefix error: \(name.rawValue)")
    }
}
private extension String {
    var isAlphanumericOrUnderscore: Bool {
        // _でCharacterSetを用意
        let underscoreCharacterSet = CharacterSet(charactersIn: "_")
        // 英数字と_のCharacterSetを結合
        let characterSet = CharacterSet.alphanumerics.union(underscoreCharacterSet)
        // 空じゃない、且つ用意したCharacterSet以外が含まれない
        return !isEmpty && rangeOfCharacter(from: characterSet.inverted) == nil
    }

    var hasPrefixLetter: Bool {
        let prefix = self[startIndex...index(startIndex, offsetBy: 1)]
        // 空じゃない、且つアルファベット以外が先頭文字ではない
        return !isEmpty && prefix.rangeOfCharacter(from: CharacterSet.letters.inverted) == nil
    }
}

Unit TestはCIでプルリク時点で走るようになってるので
新規にenumにイベント名を追加したプルリク生成時にバリデーションされるようになった。

FirebaseAnalyticsのイベント名の制約仕様に変更があった場合に追随できないのでそこはご注意。

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

SwiftUIでキーボードを下げる(非表示にする)方法

まず、下記の拡張を実装しておきます。
新しいファイル作って保存しておくと良いと思います。

UIApplication+ext.swift
extension UIApplication {
    func endEditing() {
        sendAction(
            #selector(UIResponder.resignFirstResponder),
            to: nil,
            from: nil,
            for: nil
        )
    }
}

あとは、キーボードを下げたいところで呼び出して使うだけです。

ContentView.swift
struct ContentView : View {
    var body: some View {
        Button(action: {
            // ここでキーボードを下げる
            UIApplication.shared.endEditing()
        }) {
            Image(systemName: "keyboard.chevron.compact.down")
        }
    }
}

SFアイコンを使ってキーボードを下げるボタンを実装してみた例になります。

参考

https://stackoverflow.com/questions/56491386/how-to-hide-keyboard-when-using-swiftui

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