- 投稿日:2019-10-04T22:13:28+09:00
@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: nameSwift5.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) } } }
- 投稿日:2019-10-04T19:33:30+09:00
SkeletonView
よく商用アプリでも使われているのを見るローディングライブラリである「SkeletonView」を使用したので、使い方の備忘録として書き残しておきます。
import SkeletonViewoverride 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/
- 投稿日:2019-10-04T17:12:42+09:00
新しいバージョンが利用可能になったことをユーザにお知らせする - Siren
Siren とは?
アプリの新しいバージョンが利用可能になったことをユーザにお知らせアラートを表示するライブラリです。
本内容は Siren 5.2.1 をもとに記述しています。シンプルな実装
import Siren 後、Siren.shared.wail() を didFinishLaunchingWithOptions で呼び出すだけで動作します。
AppDelegate.swiftimport 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 ユーザにアプリの更新を強制する option 今すぐアプリを更新するか、起動時に更新する
どちらかユーザが選択できるskip ・今すぐアプリを更新する
・次回の起動時に更新する
・このバージョンはスキップする
の内からいずれかをユーザが選択できるreleasedForDays
デフォルトでは更新アラートの表示を1日遅らせているそうです。
(showAlertAfterCurrentVersionHasBeenReleasedForDays: 1)
これは全ての地域の App Store CDN でバイナリが利用可能になる時間を待つためです。
通常、6〜24時間かかるため1日遅らせているようです。
showAlertAfterCurrentVersionHasBeenReleasedForDays に 0 を指定すると即時チェックとなりますがアラートが表示されるのに App Store にアップデートがまだ存在しない状況が発生する可能性があります。Localization
デフォルトではアプリの言語設定に従ってメッセージが表示されます。
設定が存在しない場合は英語で表示されます。
「設定 > 一般 > 言語と地域 > 使用言語」でメッセージが切り替わります。
言語を強制したい場合
例えばどんな場合でも日本語で表示したい場合は PresentationManager を指定します。
siren.presentationManager = PresentationManager(forceLanguageLocalization: .japanese)独自の文言で表示したい場合
あらかじめ用意されたメッセージではなくオリジナルな文言を用いたい場合も PresentationManager を設定します。
ボタンの文字色も変更できます。siren.presentationManager = PresentationManager( alertTintColor: UIColor.orange, appName: "アプリ名", alertTitle: "新しいバージョンがあります", alertMessage: "新バージョンではXXX機能がご利用頂けます", updateButtonTitle: "アップデートする", nextTimeButtonTitle: "今はしない", skipButtonTitle: "次のバージョンまで表示しない", forceLanguageLocalization: .japanese)オリジナルのメッセージには新しいバージョン番号が表示されていますが、独自の文言を設定する場合には新しいバージョン番号を差し込むことは今のところできません。
iOS13 対応
5.2.1 より古いバージョンでは、iOS13でダイアログが表示されてすぐ画面遷移してしまう不具合が発生していますので 5.2.1 以降へアップデートしましょう。
- 投稿日:2019-10-04T16:54:27+09:00
ObjectMapper failed to serialize responseが出たらJSONを疑え
はじめに
APIでJSONを取得してくる時に、静かにエラーになっていた。
ニュースアプリを作っているのですが、記事の筆者の情報が取れていなかった。環境
pod 'AlamofireObjectMapper', '~> 5.2'
pod 'ObjectMapper', '~> 3.4'
Swift4.2
Xcode11
target? iOS:12.1エラーと対処法
NetworkUtils.swiftlet 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/ここで、無駄な改行を消したらソースは変更せずに解決しました!
- 投稿日:2019-10-04T15:47:29+09:00
AppleWatchをブルブルさせようとしたら少しハマった
https://qiita.com/koogawa/items/3dcc72e77ce9e5b106b1
この記事を参考に,ブルブルさせようとしたが少しハマった話以下をとりあえずコピペするが、もちろんバージョンアップされまくってるのでエラーが出る
WKInterfaceDevice.currentDevice().playHaptic(type: WKHapticType)
currentDevice
はcurrunt
に
playHaptic
はplay
にrenameされているらしいとりあえず,これで実行してみる
WKInterfaceDevice.current().play(WKHapticType.notification)が,実行されないしなぜかエラーも出ない
WKInterfaceDevice.current().play(.notification)これで動きました
- 投稿日:2019-10-04T10:12:52+09:00
FirebaseAnalytics イベント名のバリデーション
FirebaseAnalyticsのイベント名は使用できる文字に制約があるが、無効な文字列が使われてても気づかないので、Unit Testで検知するようにした。
FirebaseAnalyiticsの導入に関しては記載しない。
- Xcode11.1
- Swift5
イベント名はenumでどこかに纏めて記述しておく。
送信箇所が多いと結構長いリストになる。enum EventName: String, CaseIterable { case tapHoge = "tap_hoge" case tapFuga = "tap_fuga" ... }バリデーション
制約は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_" から始まってはいけない
テストコード
上記条件をそのままテストコードにした。
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のイベント名の制約仕様に変更があった場合に追随できないのでそこはご注意。
- 投稿日:2019-10-04T00:00:39+09:00
SwiftUIでキーボードを下げる(非表示にする)方法
まず、下記の拡張を実装しておきます。
新しいファイル作って保存しておくと良いと思います。UIApplication+ext.swiftextension UIApplication { func endEditing() { sendAction( #selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil ) } }あとは、キーボードを下げたいところで呼び出して使うだけです。
ContentView.swiftstruct 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