- 投稿日:2020-12-06T22:55:43+09:00
WKWebViewの完了ボタンを消した話
これのこと
WebView内で入力が必要になったときに自動的についてくるらしい、タップするとキーボードを閉じてくれるが、この手のボタンはカスタムで配置するので邪魔だったので消せるようにした
やり方は以下// // WKWebView+removeDone.swift // WebViewDone import Foundation import WebKit extension WKWebView { func removeDoneButton() { guard let target = scrollView.subviews.first(where: { String(describing: type(of: $0)).hasPrefix("WKContent") }), let superclass = target.superclass else { return } let noInputAccessoryViewClassName = "\(superclass)_NoInputAccessoryView" var newClass: AnyClass? = NSClassFromString(noInputAccessoryViewClassName) if newClass == nil, let targetClass = object_getClass(target), let classNameCString = noInputAccessoryViewClassName.cString(using: .ascii) { newClass = objc_allocateClassPair(targetClass, classNameCString, 0) if let newClass = newClass { objc_registerClassPair(newClass) } } guard let noInputAccessoryClass = newClass, let originalMethod = class_getInstanceMethod(NoInputAccessoryView.self, #selector(getter: NoInputAccessoryView.inputAccessoryView)) else { return } class_addMethod(noInputAccessoryClass.self, #selector(getter: NoInputAccessoryView.inputAccessoryView), method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)) object_setClass(target, noInputAccessoryClass) } } fileprivate final class NoInputAccessoryView: NSObject { @objc var inputAccessoryView: AnyObject? { return nil } }WKWebViewにinputAccessoryViewプロパティがあるがread-onlyなため、自分で作った
NoInputAccessoryViewにSwizzleする感じ
あとはWebViewから呼ぶだけwebView.removeDoneButton()消えた。あとはOSのアップデート等でこの辺が変更にならないことを祈るのみ。
- 投稿日:2020-12-06T21:41:56+09:00
iOSアプリで位置情報取得からのバックグラウンド処理をしたときの話
これは2014年頃、Swiftが発表される直前の話。
バックグラウンドになってもどうにか処理をつづけられないかという依頼があって、いろいろためしてみたメモ。
知っての通りiOSにはバックグラウンドに厳しい制約があり基本無理だがいくつか方法はあった
BackgroundFetch(Background App Refresh Tasks)をつかう
iOSが任意のタイミングでアプリの処理を30秒だけ実行してくれる。
iOS7からだったので却下したVoIPアプリになる
VoIPアプリではないので却下位置情報をバックグラウンドで取得
他アプリでも採用されていた方法だったのでこれをやってみたいまはBackground ProcessingやBackgroundNotificationの選択肢もあるが当時はなかったので除外
位置情報取得
CoreLocation.frameworkの追加、CapabilityからBackground Modesを追加してLocation updatesにチェックをいれておく
当時はObjective-cで書いていたが、今Objective-cを残してもしょうがないのでSwiftで記載するvar locationManager: CLLocationManager = { var locationManager = CLLocationManager() locationManager.allowsBackgroundLocationUpdates = true locationManager.desiredAccuracy = kCLLocationAccuracyKilometer locationManager.distanceFilter = 1 return locationManager } ()
allowsBackgroundLocationUpdatesをtrueにしておくとバックグラウンドで受信できるfunc startLocation() { locationManager.startMonitoringSignificantLocationChanges() // 大幅に位置が変更した時のみ位置情報を取得する }
startMonitoringSignificantLocationChangesを使うと位置情報が大きく動いたときだけupdateされるextension AppDelegate: CLLocationManagerDelegate { func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { let status = UIApplication.shared.applicationState if status != .background { return } NSLog("test application update location") task.execute() } }位置情報の変更を検知したらバックグラウンドタスクを実行、今回はバックグラウンドで動いていることがわかりやすいようにタスクの実行をバックグラウンドでのみ行う。
class BackgroundTask { private var loopCount = 0 private var isExecuting = false private let PrefKey = "countValue" func execute() { if isExecuting { return } DispatchQueue.global().async { self.isExecuting = true self.loopCount = UserDefaults.standard.integer(forKey: self.PrefKey) while(true) { NSLog("test application background task %d", self.loopCount) sleep(1) self.loopCount = self.loopCount + 1 UserDefaults.standard.setValue(self.loopCount, forKey: self.PrefKey) } } } }検証用のバックグラウンドタスク、ただ1秒おきにログを表示するだけ
結果
検証はシミュレータのLocationからFree Driveをつかって行った
2020-12-06 21:28:29.373625+0900 LaunchLocationInBackground[2189:105006] test application enter foreground 2020-12-06 21:28:32.634231+0900 LaunchLocationInBackground[2189:105006] test application enter background 2020-12-06 21:28:38.703424+0900 LaunchLocationInBackground[2189:105006] test application update location 2020-12-06 21:28:38.710385+0900 LaunchLocationInBackground[2189:105264] test application background task 0 2020-12-06 21:28:39.759649+0900 LaunchLocationInBackground[2189:105264] test application background task 1 2020-12-06 21:28:40.821763+0900 LaunchLocationInBackground[2189:105264] test application background task 2 2020-12-06 21:28:41.858983+0900 LaunchLocationInBackground[2189:105264] test application background task 3 2020-12-06 21:28:42.959856+0900 LaunchLocationInBackground[2189:105264] test application background task 4 2020-12-06 21:28:43.967623+0900 LaunchLocationInBackground[2189:105264] test application background task 5 2020-12-06 21:28:45.003097+0900 LaunchLocationInBackground[2189:105264] test application background task 6 2020-12-06 21:28:46.037451+0900 LaunchLocationInBackground[2189:105264] test application background task 7 2020-12-06 21:28:47.138904+0900 LaunchLocationInBackground[2189:105264] test application background task 8 2020-12-06 21:28:48.235930+0900 LaunchLocationInBackground[2189:105264] test application background task 9 2020-12-06 21:28:49.258731+0900 LaunchLocationInBackground[2189:105264] test application background task 10 2020-12-06 21:29:32.255424+0900 LaunchLocationInBackground[2189:105006] test application update location 2020-12-06 21:29:32.255519+0900 LaunchLocationInBackground[2189:105264] test application background task 11 2020-12-06 21:29:33.323138+0900 LaunchLocationInBackground[2189:105264] test application background task 12 2020-12-06 21:29:34.425526+0900 LaunchLocationInBackground[2189:105264] test application background task 13 2020-12-06 21:29:35.434688+0900 LaunchLocationInBackground[2189:105264] test application background task 14 2020-12-06 21:29:36.526842+0900 LaunchLocationInBackground[2189:105264] test application background task 15 2020-12-06 21:29:37.535671+0900 LaunchLocationInBackground[2189:105264] test application background task 16 2020-12-06 21:29:38.622164+0900 LaunchLocationInBackground[2189:105264] test application background task 17 2020-12-06 21:29:39.658490+0900 LaunchLocationInBackground[2189:105264] test application background task 18 2020-12-06 21:29:39.698484+0900 LaunchLocationInBackground[2189:105006] test application update location 2020-12-06 21:29:40.672103+0900 LaunchLocationInBackground[2189:105264] test application background task 19 2020-12-06 21:29:41.681671+0900 LaunchLocationInBackground[2189:105264] test application background task 20 2020-12-06 21:29:42.747317+0900 LaunchLocationInBackground[2189:105264] test application background task 21 2020-12-06 21:29:43.818607+0900 LaunchLocationInBackground[2189:105264] test application background task 22 2020-12-06 21:29:44.830959+0900 LaunchLocationInBackground[2189:105264] test application background task 23 2020-12-06 21:29:45.855771+0900 LaunchLocationInBackground[2189:105264] test application background task 24 2020-12-06 21:29:46.954273+0900 LaunchLocationInBackground[2189:105264] test application background task 25 2020-12-06 21:29:48.055699+0900 LaunchLocationInBackground[2189:105264] test application background task 26 2020-12-06 21:29:49.065966+0900 LaunchLocationInBackground[2189:105264] test application background task 27 2020-12-06 21:29:50.097980+0900 LaunchLocationInBackground[2189:105264] test application background task 28
application enter backgroundでバックグラウンドに突入してから、application update locationでアプリが起動し、バックグラウンドタスクが実行されている。
また、バックグラウンド生存期間が終了したあとも位置情報の更新によって続きから実行されている。
昔試したときはバックグラウンドでの実行時間は3分あったきがするのだが短くなったのだろうか
- 投稿日:2020-12-06T20:28:21+09:00
[Swift]UITableViewでDrag&Dropする際の注意点
はじめに
UITableViewでロングタップを使って並び替えをする際に、
上手く実装できずに時間を浪費してしまったので、参考になる方がいれば嬉しいです。完成イメージ
UITableViewCellをロングタップするとDrag&Dropで並び替えができるようになります。
基本的な実装方法
詳しい実装方法自体は検索すれば見つかるので概要だけ記載します。
UITableViewにDrag&Dropできるように以下の設定をします。ViewController.swifttableView.dragInteractionEnabled = true tableView.dragDelegate = self tableView.dropDelegate = selfUITableViewDragDelegateの実装はこんな感じです。
Dragした際にセルの文字列を取得しています。ViewController.swiftextension HomeViewController: UITableViewDragDelegate { func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { let todoName = db.getItem(at: indexPath.row).text // Dragするセルの文字列を取得 let todoProvider = NSItemProvider(object: todoName as NSItemProviderWriting) return [UIDragItem(itemProvider: todoProvider)] } }Dropした際に、
tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator)が呼ばれて、
並び替えの実装をここでおこないます。ViewController.swiftextension ViewController: UITableViewDropDelegate { func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal { return UITableViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath) } func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) { // Dropした際の並び替えの実装 } }上手く実装できなかったところ
今回つまずいたところは、Dropした際に
tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator)の処理が呼ばれない問題でした。
他のfunc tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposalは
ちゃんと呼ばれることは堪忍できたので、UITableViewDropDelegateが設定できていないわけではないようです。解決方法
ロングタップのDrag&Dropで並び替えをする前に
navigationItem.leftBarButtonItem = editButtonItemでUITabelViewの左上に編集ボタンを表示して、
並び替えをできるようにしており、こちらの機能を併用していると対象のメソッドが呼ばれなかったです。
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath)などの並び替えするメソッドを削除したところ
ちゃんとtableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator)が呼ばれるました。まとめ
簡単な実装からステップアップで機能を追加して行く際に、
似たような機能のイベントが影響することもままありそうなので気をつけたいところです。参考記事
https://dev.classmethod.jp/articles/uitableview-dragdelegate-dropdelegate1/
https://developer.apple.com/documentation/uikit/drag_and_drop/adopting_drag_and_drop_in_a_table_view
- 投稿日:2020-12-06T18:16:14+09:00
【Swift】画面遷移先に変数を渡したい
どういうことか
遷移前の画面で定義した変数を遷移先の画面に渡したい。
渡す
prepareのメソッドを書いて値を渡す準備をする。
prepareと書いて出てきたものを選択すると自動補完してくれる。ViewController.swift// 渡したい変数 var face = "(´・ω・`)" override func prepare(for segue: UIStoryboardSegue, sender: Any?) { // 遷移先のコントローラ名を書く。ここではNextViewController let nextVC = segue.destination as! NextViewController // 受け取る先で定義する変数名(後述)に渡したい変数を代入する nextVC.nextFace = face }遷移先で受け取る
NextViewController.swift// 例えばLabelを設置しておいてそこで受け取る @IBOutlet weak var nextLabel: UILabel! var nextFace = "" override func viewDidLoad() { super.viewDidLoad() nextLabel.text = nextFace }この記事の場合だと遷移先の画面に設置したLabelに
(´・ω・`)が表示される。
実用的なところでは計算した答えとか遷移先に渡すとかがいいと思う。
- 投稿日:2020-12-06T16:37:37+09:00
SwiftLintで追加・廃止されたルールまとめ(Swift 5.1.2→5.3.1版)
はじめに
本記事は Swift/Kotlin愛好会 Advent Calendar 2020 の7日目の記事です。
SwiftLintで追加・変更・廃止されたルールまとめ(Swift 4.2→5.1.2版) - Qiita から2020/12/06現在の最新版までの間に、追加・廃止されたルールを解説します。
前回の記事を読んでいなくても読み進められますが、併せて読むことをオススメします。比較方法
前回の記事や公式ドキュメント、リリースノートを参考に差分を抽出しました。
変更されたルールも書こうとしたのですが、リリースノートを見ると多かったので見送りました。
ルール 数 追加 13 廃止 0 ※ソースでなくドキュメントを確認して調査したため、実際の数と異なる可能性があります。
環境
- OS:macOS Big Sur 11.0.1
- Swift:5.3.1
- Xcode:12.2 (12B45b)
- SwiftLint:0.42.0-rc.1
0.42.0は正式リリースされていない追加されたルール
Comment Spacing
- 追加バージョン:0.42.0-rc.1
- デフォルト:有効
コメントはスラッシュの後ろにスペースを1つ以上空けるべきです。
https://realm.github.io/SwiftLint/comment_spacing.html// bad //Foo ///Foo // good // Foo // Foo /// FooComputed Accessors Order
- 追加バージョン:0.40.0
- デフォルト:有効
コンピューテッドプロパティのゲッターとセッターは、統一した順序にすべきです。
https://realm.github.io/SwiftLint/computed_accessors_order.html// bad class Foo { var foo: Int { set { _foo = newValue } get { 3 } } } // good class Foo { var foo: Int { get { 3 } set { _foo = newValue } } }Inclusive Language
- 追加バージョン:0.41.0
- デフォルト:有効
識別子は人種、性別、社会経済的地位に基づく差別を避けるために、包括的な言語を使うべきです。
https://realm.github.io/SwiftLint/inclusive_language.html// bad enum BlackList { case foo case bar } func updateWhiteList(add: String) {} init(master: String, slave: String) {} // good enum DenyList { case foo case bar } func updateAllowList(add: String) {} init(leader: String, follower: String) {} enum WalletItemType { case visa case mastercard // `0.42.0-rc.1` からデフォルトで許可された }Twitterが提供している一覧が参考になります。
We’re starting with a set of words we want to move away from using in favor of more inclusive language, such as: pic.twitter.com/6SMGd9celn
— Twitter Engineering (@TwitterEng) July 2, 2020Orphaned Doc Comment
- 追加バージョン:0.38.2
- デフォルト:有効
ドキュメンテーションコメントは宣言に付けるべきです。
https://realm.github.io/SwiftLint/orphaned_doc_comment.html// bad /// My great property // Not a doc string var myGreatProperty: String! // good /// My great property var myGreatProperty: String!Enum Case Associated Values Count
- 追加バージョン:0.38.1
- デフォルト:無効
列挙型のアソシエイテッドバリューは少なくすべきです。
https://realm.github.io/SwiftLint/enum_case_associated_values_count.html// bad enum Employee { case fullTime(name: String, retirement: Date, age: Int, designation: String, contactNumber: Int) case partTime(name: String, contractEndDate: Date, age: Int, designation: String, contactNumber: Int) } // good enum Employee { case fullTime(name: String, retirement: Date, designation: String, contactNumber: Int) case partTime(name: String, age: Int, contractEndDate: Date) }IBInspectable in Extension
- 追加バージョン:0.40.0
- デフォルト:無効
エクステンションで
@IBInspectableのプロパティを追加すべきではありません。
https://realm.github.io/SwiftLint/ibinspectable_in_extension.html// bad extension Foo { @IBInspectable private var x: Int } // good class Foo { @IBInspectable private var x: Int }Indentation Width
- 追加バージョン:0.38.2
- デフォルト:無効
1つのタブまたは設定したスペースの量でコードをインデントすべきです。
https://realm.github.io/SwiftLint/indentation_width.html// bad firstLine secondLine // good firstLine secondLineNon-private XCTest member
- 追加バージョン:0.42.0-rc.1
- デフォルト:無効
テストクラスのテストでないメンバーはすべて
privateにすべきです。
https://realm.github.io/SwiftLint/non_private_xctest_member.html// bad class TotoTests: XCTestCase { var foo: Bar? func helperFunction() {} } // good class TotoTests: XCTestCase { private var foo: Bar? private func helperFunction() {} }Optional Enum Case Match
- 追加バージョン:0.38.1
- デフォルト:無効
オプショナルの列挙型は
?を付けずにマッチすべきです。
https://realm.github.io/SwiftLint/optional_enum_case_matching.html// bad switch foo { case .bar?: break case .baz: break default: break } // good switch foo { case .bar: break case .baz: break default: break }Prefer Nimble
- 追加バージョン:0.41.0
- デフォルト:無効
XCTAssertよりNimbleのマッチャーを使うべきです。
https://realm.github.io/SwiftLint/prefer_nimble.html// bad XCTAssertEqual(foo, 1) // good expect(foo) == 1Prefer Self Type Over Type of Self
- 追加バージョン:0.38.1
- デフォルト:無効
type(of: self)よりSelfを使うべきです。
https://realm.github.io/SwiftLint/prefer_self_type_over_type_of_self.html// bad class Foo { func bar() { type(of: self).baz() } } // good class Foo { func bar() { Self.baz() } }Prefer Zero Over Explicit Init
- 追加バージョン:0.40.0
- デフォルト:無効
CGPoint(x: 0, y: 0)のようなパラメータが0のイニシャライザより.zeroを使うべきです。
https://realm.github.io/SwiftLint/prefer_zero_over_explicit_init.html// bad CGPoint(x: 0.0, y: 0.0) CGRect(x: 0, y: 0, width: 0, height: 0) CGSize(width: 0, height: 0) CGVector(dx: 0, dy: 0) // good .zero .zero .zero .zeroTest case accessibility
- 追加バージョン:0.41.0
- デフォルト:無効
テストケースにはテスト以外の
privateなメンバーのみ含まれるべきです。
https://realm.github.io/SwiftLint/test_case_accessibility.html// bad class FooTests: XCTestCase { var foo: String? func foobar() {} } // good class FooTests: XCTestCase { private var foo: String? private func foobar() {} }「Non-private XCTest member」との違いがまだわかっていません。
廃止されたルール
なし
おわりに
これでSwift 5.3.1でもSwiftLintを適切に使うことができます!
以上、 Swift/Kotlin愛好会 Advent Calendar 2020 の7日目の記事でした。
明日も @uhooi の記事です。参考リンク
- 投稿日:2020-12-06T16:17:17+09:00
Swift初学者がViewController に集中しがちなロジックを ViewModel に切り出してみた
はじめに
初学者はViewControllerをfatにしがちだからViewModelを使って可読性を上げましょうという回です。
安心してください、私も初学者です。笑
どうやらView(ViewController)はUIに関することだけに留めて、データの保持やバリデーション,Api処理などの内部処理はViewModelで管理するほうが好ましいとのこと。ということで今回はログイン時におけるデータの保持とバリデーション処理をViewModelで行ってみたいと思います。
パターン① ViewControllerでバリデーション処理するときのコード
LoginViewController.swiftfinal class LoginViewController: UIViewController, UITextFieldDelegate { @IBOutlet weak var emailTextField: UITextField! @IBOutlet weak var passwordTextField: UITextField! override func viewDidLoad() { super.viewDidLoad() emailTextField.delegate = self passwordTextField.delegate = self } @IBAction private func loginButtonTapped(_ sender: Any) { // 下記でデータを格納してバリデーション処理を行っているが、これをViewModelで処理させる guard let email = emailTextField.text, let password = passwordTextField.text, isValidAccount(email, password) else { showAlert("メールアドレスまたはパスワードが有効ではありません") return } // emailTextField.textとpasswordTextField.textがnilじゃない、且つValidationがtrueを返した時、 // API,画面遷移処理(コード省略) } func isValidAccount(_ email: String, _ password: String) -> Bool { return email.count >= 6 && password.count >= 6 // 条件は適当です } // 画面タッチでキーボードを閉じる override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { view.endEditing(true) } // リターンキーを押したときにキーボードを閉じる public func textFieldShouldReturn(_ textField: UITextField) -> Bool { textField.resignFirstResponder() return true } }パターン② ViewModelでバリデーション処理するときのコード
LoginViewModelを作成
データの管理とバリデーション処理をViewControllerから移行する
LoginViewModel.swiftclass LoginViewModel { // データを格納するための変数を用意 var email: String? var password: String? // ログインビューからデータを受取るためのメソッドをつくる func setData(_ email: String, _ password: String) { self.email = email // 左辺が上で定義したemail,右辺がログインビューから受取るemail self.password = password } // 受け取ったデータでバリデーション処理をする func isLoginValidated() -> Bool { guard let email = self.email, let password = self.password, isValidAccount(email, password) else { return false } return true } func isValidAccount(_ email: String, _ password: String) -> Bool { return email.count >= 6 && password.count >= 6 // この程度の条件なら直接ぶち込んでいいかも } }LoginViewControllerを改修
LoginViewController.swiftfinal class LoginViewController: UIViewController, UITextFieldDelegate { private var loginViewModel: LoginViewModel = LoginViewModel() @IBOutlet weak var emailTextField: UITextField! @IBOutlet weak var passwordTextField: UITextField! override func viewDidLoad() { super.viewDidLoad() emailTextField.delegate = self passwordTextField.delegate = self } @IBAction private func loginButtonTapped(_ sender: Any) { guard loginViewModel.isLoginValidated() // ViewControllerでデータを保持する必要がなくなった! else { showAlert("メールアドレスまたはパスワードが有効ではありません") return } // loginViewModel.isLoginValidated()がtrueを返した時、 // API,画面遷移処理(コード省略) } // 画面タッチでキーボードを閉じる override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { view.endEditing(true) } // リターンキーを押したときにキーボードを閉じる public func textFieldShouldReturn(_ textField: UITextField) -> Bool { textField.resignFirstResponder() return true } // キーボードを閉じたときに入力データをビューモデルに渡す func textFieldDidEndEditing(_ textField: UITextField) { guard let email = emailTextField.text, let password = passwordTextField.text else { return } loginViewModel.setData(email, password) } }LoginViewController作成にあたってのポイント
①最初にLoginViewModelのインスタンスを作成しておく。
初心者あるある:各メソッド内でインスタンスを作成してしまう。⇒データの授受に失敗します。private var loginViewModel: LoginViewModel = LoginViewModel()②キーボードを閉じたときに呼び出されるメソッドを活用する
func textFieldDidEndEditing(_ textField: UITextField) { // 呼び出したい処理 }おわりに
今回の例ではコード量が少なくなったわけではないので初学者には利便性は伝わらないかもしれません。私もまだそこまで実感できていません笑
全体最適を考えられるエンジニアとしての第一歩と考えましょう!
- 投稿日:2020-12-06T15:31:31+09:00
【Realm】SwiftUIのListの並び替え(.onMove)とRealmの連携
はじめに
SwiftUIのListの並び替え(.onMove)を用いるアプリで、Realmと連携しデータ保存を行う方法をまとめました。
↓こんなサンプルアプリを作成しました。
環境
Swift 5.3.1
Xcode 12.2
Cocoapods 1.10.0
RealmSwift 10.1.4経緯
SwiftUIでRealmを使用し始め、Listの並び替えの .onMove が中々上手く動作させられなかったので、同じような悩みの人もいるのでは?と思い書きました。
SwiftUIのListの並び替えをRealmに対応させる情報は見つけられなかった(無くは無いが、私には理解できなかった)ので、私でもできる方法が無いか模索しました。
その結果、UITableViewとRealmの例を参考にすることで、実装することができました。実装した動作・機能
- リストの追加
- テキストフィールドに文字を入力し、決定ボタンでその文字をリストに追加。
- EditButton
- EditModeのオンオフができるボタン。
- リストの並び替えや削除を行うために、SwiftUIにあらかじめ準備されているもの。
- リストの並び替え
- EditModeがオンの時、Listの並び替えを可能に。
- リストの削除
- リストの左スワイプ、及びEditModeがオンの時、削除用のボタンタップで削除。
- Realmで保存
- DBにデータを保存。アプリを落としてもデータが保存されている。
ソースコード
RealmDBのデータを定義
- id : DBの主キー用のID
- title : リストに表示するタイトル
- order : リストを並び替える際に使用する数値
ポイントはorderを入れることです。このorderを変化させることで、並び替えを実現できました。
ItemDB.swiftimport RealmSwift class ItemDB: Object { @objc dynamic var id = "" @objc dynamic var title = "" @objc dynamic var order = 0 override class func primaryKey() -> String? { "id" } }RealmDBと同じ要素を持つ構造体Itemを作成
Item.swiftimport Foundation struct Item: Identifiable { let id: String let title: String let order: Int } extension Item { init(itemDB: ItemDB) { id = itemDB.id title = itemDB.title order = itemDB.order } }ItemStoreの作成
ここのitemsを用いて、DBのデータを取ってきています。
.sorted(byKeyPath: "order")で、orderの数値を用いて、itemResultsを並び替えています。ItemStore.swiftimport RealmSwift final class ItemStore: ObservableObject { private var itemResults: Results<ItemDB> init(realm: Realm) { itemResults = realm.objects(ItemDB.self) .sorted(byKeyPath: "order") // orderの数値で並び替え } var items: [Item] { itemResults.map(Item.init) } }ItemStoreには、Realmを操作する関数も記述しています。
ItemStore.swiftextension ItemStore { // データの追加 func create(title: String, order: Int) { objectWillChange.send() do { let realm = try Realm() let itemDB = ItemDB() itemDB.id = UUID().uuidString itemDB.title = title itemDB.order = order try realm.write { realm.add(itemDB) } } catch let error { print(error.localizedDescription) } } // データの削除 func delete(id: String) { objectWillChange.send() guard let itemDB = itemResults.first(where: { $0.id == id }) else { return } do { let realm = try Realm() try realm.write { realm.delete(itemDB) } } catch let error { print(error.localizedDescription) } } // データの更新 func update(id: String, order: Int) { objectWillChange.send() do { let realm = try Realm() try realm.write { realm.create(ItemDB.self, value: ["id": id, "order": order], update: .modified) } } catch let error { print(error.localizedDescription) } } // Listを並び替えるための関数 func move(sourceIndexSet: IndexSet, destination: Int) { guard let source = sourceIndexSet.first else { return } // 並び替える行のIDを取得 let moveId = items[source].id // source、destinationの値については、参考資料を参考にしてください。 // Listの行を下に移動する場合 if source < destination { for i in (source + 1)...(destination - 1) { update(id: items[i].id, order: items[i].order - 1) } update(id: moveId, order: destination - 1) // Listの行を上に移動する場合 } else if destination < source { // reversed()で逆から回さないと、一時的にorderの数値が重なり、想定外の挙動を示します。 for i in (destination...(source - 1)).reversed() { update(id: items[i].id, order: items[i].order + 1) } update(id: moveId, order: destination) } else { return } } }Listを表示するItemListViewの作成
ItemListView.swiftimport SwiftUI struct ItemListView: View { @EnvironmentObject var store: ItemStore @State var title = "" var items: [Item] var body: some View { VStack { EditButton() .padding(.top) HStack { TextField("タイトルの入力", text: $title) .textFieldStyle(RoundedBorderTextFieldStyle()) Button("決定", action: create) } .padding(.leading) .padding(.trailing) List { ForEach(items) { item in HStack { Text(item.title) Spacer() // orderの番号が分かりやすいように表示 Text("order:\(item.order)") } } .onDelete { offsets in delete(offsets: offsets) } .onMove { source, destination in move(sourceIndexSet: source, destination: destination) } } } } }ItemListViewに関数(create, delete, move)を記述。
ItemListView.swiftextension ItemListView { private func create() { store.create(title: self.title, order: items.count) self.title = "" } private func delete(offsets: IndexSet) { guard let index = offsets.first else { return } // 削除する行のIDを取得 let deleteId = items[index].id // 削除する行の行番号を取得 let deleteOrder = items[index].order // 削除する行の行番号より大きい行番号を全て -1 する for i in (deleteOrder + 1)..<items.count { store.update(id: items[i].id, order: items[i].order - 1) } // 行を削除する store.delete(id: deleteId) } private func move(sourceIndexSet: IndexSet, destination: Int) { store.move(sourceIndexSet: sourceIndexSet, destination: destination) } }ContentsViewとSceneDelegateの書き換え
ContentsView.swiftstruct ContentView: View { @EnvironmentObject var store: ItemStore var body: some View { ItemListView(items: store.items) } }SceneDelegate.swift// 一部抜粋 // import RealmSwiftしてください。 func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { if let windowScene = scene as? UIWindowScene { do { let realm = try Realm() let window = UIWindow(windowScene: windowScene) let contentView = ContentView() .environmentObject(ItemStore(realm: realm)) window.rootViewController = UIHostingController(rootView: contentView) self.window = window window.makeKeyAndVisible() } catch let error { fatalError("Failed to open Realm.Error:\(error.localizedDescription)") } } }これで一通り完了です。
EditModeにして並び替えてみると、orderの数値の変化に伴って並び変わります。
以上となります!参考資料
この記事でorderを追加する着想を得ました。こちらUITableViewでの例です。
realmをデータソースにしてテーブルビューの並べ替えをしたい(stackoverflow)この記事でonMoveのsourceとdestinationの示す値について勉強しました。
【SwiftUI】onMove時の destination の値が並び替え方向の違いで異なる謎。SwiftUIでRealmを使う分かりやすいサンプルを作れる記事です。
Listの並び替えとは関係ないですが、コードの書き方全般はこの記事に則っています。
Realm with SwiftUI Tutorial: Getting Started(raywenderlich.com)おわりに
最後まで読んでくださりありがとうございます。
私はListの並び替えで結構悩んだので、皆さんのその時間をすっ飛ばせるような記事になっていれば幸いです。参考資料のUITableViewの例で、RealmのListを用いる手法も挙げられていました。
しかし、私自身のRealmの知識が乏しく、理解できておりません。
その手法を用いた方が、より簡単に並び替えを実装できるかもしれないので、その検討も随時進めていきます。
逆に、これ良いよ!的なのがありましたら教えてくださると嬉しいです!最後に、SwiftUIとRealmで追加・削除・更新を行うサンプルアプリの記事も書いています。
SwiftUIでRealmを使う導入的な内容です。良かったら見てみてください。
SwiftUIでRealmを使ってみた
- 投稿日:2020-12-06T14:33:58+09:00
条件分岐文について〜guard文〜
guard文 とは
条件不成立時に早期退出を行うための条件分岐になります。
if文と同じく条件式の結果であるBool型の値に応じた処理を行います。
ですがif文とは異なり、後続の処理を行うにあたってtrueとなっているべき条件を指定します。guard文は、条件式がfalseを返す場合のみ{ }内の処理を実行し、
条件式がtrueを返す場合は{ }内の処理をスキップします。また、{ }内の文では、guard文が記述されているスコープの外に退出する必要があります。
退出する方法は、returnを用いて退出します。guard 条件式 else { 条件式がfalseだった場合の処理 return // guard文が記述されているスコープの外に退出する処理 }結果if文でよくないか?と思いがちですが、
guard文にはguard文の良さがあります。例として年齢をチェックするメソッドを作成します。
18歳以上か18歳未満かで処理を変えます。
(メソッドについての詳細は別記事で紹介する予定です。)// if文を使った場合 func ageCheckIf(age: Int) { if age >= 18 { print("18歳以上なので閲覧可能です。") } else { print("18歳以下なので閲覧できません。") } } // guard文を使った場合 func ageCheckGuard(age: Int) { guard age >= 18 else { print("18歳以下なので閲覧できません。") return } print("18歳以上なので閲覧可能です。") } ageCheckIf(age: 10) ageCheckGuard(age: 10) ageCheckIf(age: 20) ageCheckGuard(age: 20) 実行結果 18歳以下なので閲覧できません。 // ageCheckIf(age: 10) 18歳以下なので閲覧できません。 // ageCheckGuard(age: 10) 18歳以上なので閲覧可能です。 // ageCheckIf(age: 20) 18歳以上なので閲覧可能です。 // ageCheckGuard(age: 20)文のボリューム自体は大体同じですが、
見やすさがguard文の方が見やすいかなと個人的には思います。guard文についての説明ですが、
guard age >= 18 elseの部分で18歳以上かどうかチェックしています。
もしageが18未満だった場合はguard文の中に入ります。guard文の中では、
print("18歳以下なので閲覧できません。")を実行後、
returnでスコープの外に退出しています。どこまで退出するのかというと、guard文が存在するスコープの外まで退出します。
つまり、func ageCheckGuard(age: Int) {・・・}の外までです。guard文の条件式の結果がtrueだった場合は、
guard文の{ }内は実行されず、guard文以降の処理が実行されます。つまり、guard文以降の処理は全て、
guard文の結果がtrueだった時の処理ということになります。条件分岐でfalseの時だけこの処理をしたい!などの場面ではかなり使えると思います。
全てif文で記述するとネストしすぎて見づらくなる可能性もあります。退出の強制
guard文のelse節はguard文が含まれるスコープから絶対に退出しなければなりません。
スコープからの退出は強制されているのでコンパイラにチェックされております。
なので、{ }内にreturnがないとコンパイルエラーが発生します。強制されているが故に、
guard文以降の処理はguard文の条件式が必ず成り立っていることが証明されます。なので、先ほどの年齢チェックの例でも、
条件式がtrueだった場合のguard文以降では、年齢が18歳以上と証明されている状態になります。正直ここまでの内容だったらif文と大差ないので好きな方を使いたくなりますが、
ここからがguard文の真骨頂になります。guard-let文
if文と同様にguard文もguard-let文が利用できます。
処理の流れ的にはif-let文と同じなのですが、大きな違いが一つあります。それは、guard-let文で宣言された変数や定数は、
guard-let文以降でも利用可能という点です。// if-let文の定数のスコープ func makeNameIf(string: String?) { if let name = string { print(name + "さんこんにちは。") } else { print("nilです。") } print(name + "さんこんにちは。") // コンパイルエラー } //guard-let文の定数のスコープ func makeNameGuard(string: String?) { guard let name = string else { print("nilです。") return } print(name + "さんこんにちは。") } makeNameIf(string: "太郎") // コンパイルエラーで実行できない makeNameGuard(string: "太郎") 実行結果 太郎さんこんにちは。このようにif-let文の場合は、定数をif-let内でしか利用できないので、
if-let文の外で使いたい場合は別で変数を宣言しておく必要があります。逆にguard-let文の場合は、guard-let文以降でも利用することができるので、
別で変数を宣言する必要がなくなります。では、コードの違いがわかるように、
同じ機能を持ったif-let文とguard-let文を記述してみます。どちらも、Optional<Int>型の値を2つ引数に持ち、
返り値もOptional<Int>型の返り値を持ちます。第一引数もしくは第二引数がnilの場合は、
第一(第二)引数に値が入っていません。と表示し処理を終える流れになります。func add(_ intA: Int?, _ intB: Int?) -> Int? { let a: Int let b: Int if let wrappedIntA = intA { a = wrappedIntA } else { print("第一引数に値が入っていません") return nil } if let wrappedIntB = intB { b = wrappedIntB } else { print("第二引数に値が入っていません") return nil } return a + b } add(10, 10) // 20if文の場合は、if-let文で宣言した定数はif-let文内でしか使用できないので、
定数aと定数bを別で宣言しそれに対して代入を行っています。それに対してguard-let文は次のようになります。
func add(_ intA: Int?, _ intB: Int?) -> Int? { guard let a = intA else { print("第一引数に値が入っていません。") return nil } guard let b = intB else { print("第二引数に値が入っていません。") return nil } return a + b } add(10, 10) // 20guard文の場合は、guard-letで宣言した定数を使うことができるので
別で宣言する必要がなくなります。このように、条件に応じて早期退出するコードの場合はguard文を使用して実装した方が、
シンプルなコードを記述することができます。また、guard文の場合は退出処理を記述しないとコンパイルエラーになるので
単純にミスを防ぐこともできます。if文にもguard文にも違った良さがあるので、
是非二つとも理解して使い分けれるようにしてください!私もこの記事を書いてていろいろと発見があったのでお互い頑張りましょう!
以上、最後までご覧いただきありがとうございました。
- 投稿日:2020-12-06T12:57:54+09:00
SwiftUI sheetの実装での注意点
SwiftUI sheetの実装での注意点
うまくsheetが起動しなくてハマったポイントがあったので、共有したいと思います。
import SwiftUI struct ContentView: View { @State var isShowButton1: Bool = false @State var isShowButton2: Bool = false var body: some View { VStack(alignment: .center, spacing: 20, content: { // Button 1 Button(action: { isShowButton1.toggle() }, label: { Text("Button 1") }) // Button 2 Button(action: { isShowButton2.toggle() }, label: { Text("Button 2") }) }) .sheet(isPresented: $isShowButton1, content: { Text("Button 1") }) .sheet(isPresented: $isShowButton2, content: { Text("Button 2") }) } }このようにButtonを2つ縦に配置してそれぞれにSheetを追加してみたのですが、sheetがうまく作動しません。
それは、2つの.sheetが同じViewをアタッチしているかららしいです!
最後に、自分はこの実装にハマってしまい、昨日ずっと試行錯誤していました。(笑)
誰かのためになればいいなと!思います!最後まで読んでいただき、ありがとうございました!
[参照]
https://stackoverflow.com/questions/57631666/swiftui-issues-with-sheet-modifier
- 投稿日:2020-12-06T12:53:35+09:00
条件分岐文について〜if文〜
if文 とは
if文とは制御構文の中の条件分岐文の一つです。
条件の成否に応じて実行する処理を切り替える制御構文になります。if文は、Bool型の値を返す条件式とそれに続く{ }内の実行文で構成されます。
条件式がtrueを返すと{ }内の処理が行われ、falseを返すと{ }内の処理が飛ばされます。また、条件式の返す値がBool型意外だとコンパイルエラーが起こります。
今回は、
a == 1が条件式になります。定数aの値は1なので、条件式は
1 == 1となりtrueを返します。if 条件式 { 条件式がtrueだった場合に実行される処理 } -------------------- let a = 1 if a == 1 { print("aの値は1です。") } 実行結果 aの値は1です。他にも比較演算子を使用してBool型の返り値をもらう方法もあります。
同値性や比較演算子についてわからなかったら過去の記事で説明してるのでご覧ください。let value = 10 if value >= 5 { print("valueの値は5以上です。") } 実行結果 valueの値は5以上です。これだけでも十分便利なのですが、if文はまだまだ多くの機能を持っています。
else
if文は条件式がtrueだった場合の処理を指定する条件分岐文でした。
trueだけでなく、条件式がfalseだった場合の処理を指定するには、if文にelseを追加します。if 条件式 { 条件式がtrueだった時の処理 //falseだとこの{}内は読まれません } else { 条件式がfalseだった時の処理 //trueだとこの{}内は読まれません }条件式がtrueの場合は、そのすぐ次の{ }内の処理を実行。
条件式がfalseの場合は、elseの次の{ }内の処理を実行します。つまり、必ずどちらかが実行されることになります。
今回の条件式は、valueの値が5以上かどうか比較しています。
条件式は、4 >= 5なのでfalseが返り値になります。なのでelseの後の{ }内が実行されます。
let value = 4 if value >= 5 { print("valueの値は5以上です。") } else { print("valueの値は5以下です。") } 実行結果 valueの値は5以下です。では、さらに様々な条件に分けたい時はどうするか。
方法はいくつかあります。その中のよく使われる構文としては
else-if文があります。else-if文
if 条件式1 { 条件式1がtrueだった場合の処理 } else if 条件式2 { 条件式1がfalseで、条件式2がtrueだった場合の処理 } else if 条件式3 { 条件式1と条件式2がfalseで、条件式3がtrueだった場合の処理 } else { 全ての条件式がfalseだった場合の処理 }このように様々な条件を記載しそれに応じた適切な処理を実装することができます。
次のコードは、
国語のテストにて点数に応じて自動でコメントを付けてくれるコードという想定です。let score = 70 if score == 100 { print("100点満点!すごい!") } else if score >= 80 { print("高得点だ、次も頑張ろう!") } else if score >= 60 { print("頑張ったね!") } else if score >= 40 { print("もう少し頑張ろう!") } else { print("赤点です。再試験を受けてください。") } 実行結果 頑張ったね!このif文において、最初に成立する条件式は3つ目になりますので、
実行結果としては「頑張ったね!」が表示されます。if文の条件式の順番を逆に書いたとするとこうなります。
let score = 70 if score >= 40 { print("もう少し頑張ろう!") } else if score >= 60 { print("頑張ったね!") } else if score >= 80 { print("高得点だ、次も頑張ろう!") } else if score == 100 { print("100点満点!すごい!") } else { print("赤点です。再試験を受けてください。") } 実行結果 もう少し頑張ろう!条件式としては60点以上の処理内容が適切ですが、
条件式を記述する順番がおかしいので、2つ目よりも先に1つ目の条件式で成立します。ただ条件式を記述するのではなく、
臨んだ処理が行われるような順で条件式を記述していきましょう。if-elseの他にもif文の中にif文を書いて条件を分岐することもできます。
先ほどの国語のテストを例にします。let score = 70 if score >= 40 { if score >= 60 { if score >= 80 { if score == 100 { print("100点満点!すごい!") } else { print("高得点だ、次も頑張ろう!") } } else { print("頑張ったね!") } } else { print("もう少し頑張ろう!") } } else { print("赤点です。再試験を受けてください。") } 実行結果 頑張ったね!if文の中にif文を入れる
入れ子構造(ネスト)になっております。流れとしては、
if score >= 40でtrueなので{ }内へ
{ }の中にはさらにif文がありif score >= 60でこちらもtrue
さらに{ }内にif文がありif score >= 80でこちらはfalse
この時にelse側の処理を実行します。つまりprint("頑張ったね!")ややこしいですがこのような流れになります。
一つの方法ではありますが、
どこに対しての{ }なのかがよくわからなくなり記載ミスをしてしまいがちです。
一応紹介しましたが、基本はelse-ifの方がいいと思います・・・(笑)if-let文
if-let文は、以前投稿したアンラップの記事で登場しております。
そうです。if-let文はオプショナルバインディングを行う条件分岐文ですね!if-let文は、Optional<Wrapped>型の値の有無に応じて分岐を行い、
値が存在する場合のは値の取り出しも同時に行います。if-let文の構造は、ifの後に定数を宣言する形で記述します。
また、右辺にはOptional<Wrapped>型の値を記述します。Optional<Wrapped>型の中に値が入っていたら定数に値を代入します。
値が入っていた時のみ{ }内を実行し、値がnilだった場合はelse以降を実行します。今回の定数scoreのスコープはローカルスコープで、if文の{ }内でしか使用できません。
また、scoreの型はOptional<Int>ではなくInt型です。let a: Int? = 70 if let score = a { print(score) } else { print("nilです。") } 実行結果 70if-let文では、同時に複数の値を取り出すことも可能です。
複数記述する場合は,(カンマ)を使用して区切ります。また、複数指定した場合は一つでもnilが存在したらfalseになりelse側を実行します。
通常のif文でいう&&(〜かつ)のような役割をしています。let a: Int? = 70 let b: Int? = 10 if let scoreA = a, let scoreB = b { let sum = scoreA + scoreB print(sum) } else { print("nilです。") } 実行結果 80また、if-letの右辺ではダウンキャストを行うことも可能です。
if-letとas?演算子を合わせることで、型による条件分岐を安全に行えます。正直この発想はなかったので勉強になりました!
定数aはInt方をAnyでアップキャストしていますので
ダウンキャストはInt型にしないとnilが返されます。今回は、Int型でダウンキャストしているので、
定数scoreにはInt型の値である70が代入されます。let a: Any = 70 if let score = a as? Int { print(score) } else { print("nilです。") } 実行結果 70逆にString型でダウンキャストしようとするとこうなります。
let a: Any = 70 if let score = a as? String { print(score) } else { print("nilです。") } 実行結果 nilです。String型でダウンキャストができなかったので、
定数scoreにはnilが代入されました。ダウンキャストなどについてわからない場合は、
そちらも以前記事を投稿したのでご覧ください。if文やオプショナルバインディングはよく使用するので
是非覚えておきましょうー!if文関連のみでかなりのボリュームになってしまいましたが、
最後までご覧いただきありがとうございました。
- 投稿日:2020-12-06T12:25:58+09:00
App Store Small Business Programを申請してみた
App Store Small Business Programとは?
年間収益が100万ドル以下のデベロッパーに対して、有料AppおよびApp内課金への手数料が、30%から15%に引き下げられるプログラムです。
Apple Developer ニュースとアップデート
App Store Small Business Programを発表
App Store Small Business Programへの登録受付開始申請手順
App Store Connectにサインインします。
「Review the updated Paid Applications Schedule.」の警告が表示されている場合は「Agreements, Tax, and Banking」をクリックして同意を行います。
これは、今までにも有料アプリの規約が変更になった際に行っていた手続きです。上の警告が表示されなくなったら「Learn More」をクリックします。
App Store Small Business Programの概要説明ページが表示されます。
スクロールしていくと以下の画面にたどり着きます。
「Enroll」をクリックします。
冒頭の警告表示に対する確認項目と思われます。
「Yes, I have accepted.」チェックボックスをオンにします。
続いて、「他の関連する開発者アカウントを保持しているか」に関する確認項目があります。
いくつかの項目があります。以下のような背景に基づく確認項目のようです。
TechCrunch Japanより引用:
12月31日以降にプログラムが開始されると、参加デベロッパーは他のアカウントとの間でアプリを移せなくなる。
これはおそらく、開発者による「このアプリは儲かりすぎたため、早く他のアカウントに切り替えてしまおう」といった行為を防ぐためだ。
「2020年12月31日以降にアプリを移そうとした場合、または2020年12月31日以降に開始されたアプリの移転を受け入れた場合、プログラムに参加する資格がなくなります」と、アップルは記述している複数の開発者アカウントを管理している場合、それらを特定するようにアップルは求めている
私の場合は個人の開発者であり他に関連するアカウントはないため、全て「No」を選択しました。
チェックボックスをオンにして「Submit」をクリックします。
以上で手続きは完了のようです。
Apple Developerから「We’ve received your request to join the App Store Small Business Program.」というタイトルのメールが飛んできます。
We’ll review your details and let you know your enrollment status by December 30, 2020.
メール本文によると、2020年12月30日に審査結果が出るようです。
ご注意:
本記事執筆時点ではまだ審査が終わっていませんので、上記が正しい手順であることを証明できません。
申請の際は自己責任でお願いします。
- 投稿日:2020-12-06T12:15:55+09:00
Xcode 12でのビルドが2021年4月から必須になる件と、Xcode 11から12への移行事例
情報
2021年4月移行、アプリ審査の提出時にはXcode 12ビルドが必須になります。
https://developer.apple.com/jp/ios/submit/
2021年4月以降、App Storeに提出するiOS AppとiPadOS Appはすべて、Xcode 12およびiOS 14 SDKでビルドしなければなりません。
事例
私が現在担当しているアプリでも移行を実施しましたので、結果を共有します。
移行作業は例年に比べると軽微でした。移行環境
- Xcode 11.3 → 12.1
アプリ概要
- ファイナンス系
- 画面数:約120
- Swiftステップ数:約85,000
- 使用Framework
- WebKit
- StoreKit
- LocalAuthentication
- PassKit
- WatchConnectivity
- UserNotifications
ライブラリ
ライブラリ管理ツール(Carthage)への影響
- CarthageビルドがXcode 12で失敗する問題があります。公式リポジトリで回避方法が公開されていますので、それを使うことにしました。
- Xcode 12でビルドするためのシェルスクリプトが公開されているので、それを使うという回避方法で、ソースの修正等は必要ありませんし、Cartfileや導入ライブラリに対しての作業は特にありませんでした。
導入済みライブラリへの影響
11.3時代のバージョンそのままでも、特に問題はありませんでした。
- Firebase Analytics - 6.32.0
- Firebase Crashlytics - 6.32.0
- Lottie - 3.1.4
- KeychainAccess - 3.2.0
- RealmSwift - 3.18.0
ソースコードへの影響
Xcode 12のサジェスチョンに従って自動FIXした箇所が数カ所ありました。
以下のような、「再代入していないvar変数をletに変更」といった軽微なもので、コンパイルはさくっと通りました。- var foo = self.bar + let foo = self.bar
- 投稿日:2020-12-06T11:13:30+09:00
apple/swift-logコンパチのSwiftログライブラリ「Puppy」をつくった
先日、PuppyというSwiftログライブラリをつくりました。本ライブラリは、apple/swift-logのREADMEにもapple/swift-logのバックエンドコンパチログライブラリの1つとしても記載されています。また、単体の(例えばiOS向けの)ログライブラリとしても使うことができるようになっており、LinuxとDarwin(macOS/iOS/tvOS/watchOS)のいずれの環境でも動作します。サーバーサイドSwiftに興味のある方はもちろんのことiOSアプリ等を開発をする方にも一度使ってみてもらえればと思います。
https://github.com/sushichop/Puppy
※ STARをもらえると励みになります?なぜつくったのか?
私はObjective-C全盛時代によく使われていたログライブラリCocoaLumberjackのメンテナの1人として今現在もメンテナ活動を続けています。
少なくとも昔はよくできたライブラリだとは思いますが、Swift環境では使いにくい、Linux環境では使えない等の問題があります。
私が必要としていたのは、1つのライブラリでLinux、Dwarinのいずれの環境でも十分に使えること、及びログローテーションをサポートしていることでした。特に必要としていたのはLinux環境におけるファイルログローテーション機能です。
既存のOSSのSwiftログライブラリでは私の需要を満たせなかったので、自ら作ることにしました。
ライブラリ名 Linuxサポート syslog(Linux)サポート ログローテーション機能 apple/swift-logサポート SwiftyBeaver ✅ N/A N/A N/A XCGLogger N/A N/A ✅ N/A Puppy ✅ ✅ ✅ ✅ 動作環境
- Swift 5.0以上(Swift 5.3以上推奨)
- CocoaPods、Carthage、Swift Package Manager
使い方
ロギング先としてコンソール、ファイル、syslog(Linux)等をサポートしています。カスタムログフォーマットにも対応しています。詳しくはsushichop/PuppyのREADMEを参照してもらえればと思いますが、以下に使い方の例を2つ簡単に書いておきます。
使い方その1(ファイルログローテーション)
下記はファイルへロギングしつつ、10MBのログファイルを5つまでログローテーションする例です。
FileRotationLoggerの第1引数にはIDとなる逆FQDN形式の文字列を指定してください。第2引数にはファイル名を指定します。ログローテーション完了通知についてはdelegateで実現しています。import Puppy class ViewController: UIViewController { let delegate = SampleFileRotationDelegate() override func viewDidLoad() { super.viewDidLoad() let fileRotation = try! FileRotationLogger("com.example.yourapp.filerotation", fileURL: "./rotation/foo.log") fileRotation.maxFileSize = 10 * 1024 * 1024 fileRotation.maxArchivedFilesCount = 5 fileRotation.delegate = delegate let log = Puppy() log.add(fileRotation) log.info("INFO message") log.warning("WARNING message") } } class SampleFileRotationDelegate: FileRotationLoggerDeletate { func fileRotationLogger(_ fileRotationLogger: FileRotationLogger, didArchiveFileURL: URL, toFileURL: URL) { print("didArchiveFileURL: \(didArchiveFileURL), toFileURL: \(toFileURL)") } func fileRotationLogger(_ fileRotationLogger: FileRotationLogger, didRemoveArchivedFileURL: URL) { print("didRemoveArchivedFileURL: \(didRemoveArchivedFileURL)") } }使い方その2(apple/swift-logバックエンド)
apple/swift-logバックエンドとして使う場合は、Carthageは利用できません。これはapple/swift-logがCarthageをサポートしていないためです。CocoaPodsまたはSwift Package Managerを使ってインテグレーションしてください。
下記はコンソールとsyslogへロギングする例です。Puppyに加えてLoggingもインポートする必要がある点に注意ください。import Puppy import Logging let console = ConsoleLogger("com.example.yourapp.console") let syslog = SystemLogger("com.example.yourapp.syslog") let puppy = Puppy.default puppy.add(console) puppy.add(syslog) LoggingSystem.bootstrap { var handler = PuppyLogHandler(label: $0, puppy: puppy) // Set the logging level. handler.logLevel = .trace return handler } log.trace("TRACE message") // Will be logged. log.debug("DEBUG message") // Will be logged.その他(補足)
本ライブラリでは、あえて実装していない機能等もあります。その1つが
__dispatch_queue_get_label(nil)関数によるDispatchキューラベル取得機能です。これは、Linux環境で使えないこと、及びここにもあるように本関数を呼び出すことにより、潜在的にクラッシュする可能性があるためです。
どうしても使いたい場合は、本ライブラリではLogFormattableプロトコルによるカスタムログフォーマットに対応していますので、ライブラリ利用者は必要に応じて本プロトコル実装の中で、上記関数を呼び出すことにより対応できます。最後に
今回つくったSwiftログライブラリPuppyについては、Linux/Darwinの両環境で動作することを重視しており、今後も適宜改版していく予定です。サーバサイドSwift盛り上がっていくといいですね?
- 投稿日:2020-12-06T11:04:37+09:00
【Swift】 絶対値に変換する方法【初心者】
はじめに
絶対値にする方法がわからなかったので、調べた事をメモとして記録しておこうと思います。
絶対値にする(Int型)
absを使用
//Int型 let numberA:Int = -1 print(numberA)//-1 //絶対値にする print(abs(numberA))//1絶対値にする(Double型)
fabsを使用
//Double型 let numberB:Double = -1.0 print(numberB)//-1.0 //絶対値にする print(fabs(numberB))//1.0絶対値にする(Float型)
fabsfを使用
//Float型 let numberC:Float = -1.0 print(numberC)//-1.0 //絶対値にする print(fabsf(numberC))//1.0終わりに
初学者のため、もし間違いがあれば教えていただければ幸いです。
- 投稿日:2020-12-06T02:33:58+09:00
スクロールでNavigationBarとTabBarを隠す機能を自作してみる
はじめに
この記事はand factory Advent Calendar 2020 の6日目の記事です。
昨日は@myoshitaさんのNavigation Component with Kotlin DSLでした!今回は、iOSでスクロール時にNavigationBarとTabBarを隠したり表示したりする機能を自作してみましたので
実装の仕方をまとめてみようと思います。環境
開発環境は以下の通りです。
Tool Version Xcode 12.2 Swift 5.3.1
Target iOS 13 or later
Interface Storyboard (UIKit) ※ 本投稿ではSwiftUI版の内容は含んでおりませんm(_ _)m
完成イメージ
スクロール方向に応じて、Barを隠したり表示したりします
dragging scrolling 実装の方針について
スクロールに応じてNavigationBarを隠したり表示したりする機能はいくつかOSSがあったのですが、
Barを隠した分TableViewをうまくリサイズできなかったりしたので
勉強も兼ねてスクラッチで実装してみようと思い、やってみました!※ OSSに関しては、自分が実装の仕方誤っていただけかもなので、本記事では特に扱いませんm(_ _)m
実装の流れ
大まかには以下のような実装になります。
スクロール開始時にスクロール位置を保持
↓
スクロール量に応じて、NavigationBarを上に、TabBarを下にそれぞれ移動させる
↓
ScrollView(サンプルではTableView)のTopとBottomのConstraintを更新するAutoLayoutの設定
特に意識せずにTableViewのAutoLayoutを設定するとSafeAreaに対して上下の制約を設定してしまいますが
今回は、Superviewに対して制約を設定します。設定した制約をソースコードに紐付けて、制約を制御することで
NavigationBarとTabBarが表示されている場合は、Barの高さ分だけ制約でスペースを開けて
NavigationBarとTabBarが隠されている場合は、画面の端までTableViewを広げられるようになります。実装
複数の画面で同じ実装をすることになりそうだったので、
HideBarManagerというクラスを作って、ViewControllerから必要なプロパティを渡して
スクロール量の計算や制約の更新をします(*・ω・*)初期処理
final class HideBarManager { /// スクロールビューのTopの制約 /// /// - Note: Topの制約は、SafeAreaInsets.TopではなくSuperview.Topに対して設定してください private weak var topConstraint: NSLayoutConstraint? /// スクロールビューのBottomの制約 /// /// - Note: Bottomの制約は、SafeAreaInsets.BottomではなくSuperview.Bottomに対して設定してください private weak var bottomConstraint: NSLayoutConstraint? /// スクロールビュー private weak var scrollView: UIScrollView! /// タブバー private weak var tabBar: UITabBar? /// ナビゲーションバー private weak var navigationBar: UINavigationBar? /// タブバーのフレーム private var tabBarDefaultFrame: CGRect! /// ナビゲーションバーのフレーム private var navigationBarDefaultFrame: CGRect! /// スクロール開始時の位置 private var scrollBeginningOffsetY: CGFloat? /// タブバーの移動量 private var tabBarMovingDistance: CGFloat = 0 /// ナビゲーションバーの移動量 private var navigationBarMovingDistance: CGFloat = 0 /// スクロール方向 private var scrollDirectionY = UIScrollView.ScrollDirectionY.none private init() {} static func build(topConstraint: NSLayoutConstraint?, bottomConstraint: NSLayoutConstraint?, scrollView: UIScrollView!, tabBar: UITabBar?, navigationBar: UINavigationBar?) -> HideBarManager { let hideBarManager = HideBarManager() hideBarManager.topConstraint = topConstraint hideBarManager.bottomConstraint = bottomConstraint hideBarManager.scrollView = scrollView hideBarManager.tabBar = tabBar hideBarManager.navigationBar = navigationBar return hideBarManager } }VC側
private lazy var hideBarManager = HideBarManager.build( topConstraint: self.tableViewTopConstraint, bottomConstraint: self.tableViewBottomConstraint, scrollView: self.tableView, tabBar: self.tabBarController?.tabBar, navigationBar: self.navigationController?.navigationBar )ライフサイクル処理
ViewControllerの各ライフサイクルのメソッドが呼び出されるタイミングで実行する処理をHideBarManagerに実装していきます。
extension HideBarManager { func viewDidLoad() { self.addObservers() } func viewDidLayoutSubviews() { // 初回のみ代入したいためnilチェックもする if let tabBar = self.tabBar { self.tabBarDefaultFrame = self.tabBarDefaultFrame ?? tabBar.frame } if let navigationBar = self.navigationBar { self.navigationBarDefaultFrame = self.navigationBarDefaultFrame ?? navigationBar.frame } self.updateConstraints() } func viewWillDisappear() { self.showBarImmediately() } } extension HideBarManager { private func addObservers() { // アプリ復帰時にナビゲーションバーを表示しておくために、非アクティブになる直前に表示しておく NotificationCenter.default.addObserver(self, selector: #selector(showBarImmediately), name: UIApplication.willResignActiveNotification, object: nil) } /// タブバーとナビゲーションバーを表示させる @objc private func showBarImmediately() { self.tabBar?.frame = self.tabBarDefaultFrame self.navigationBar?.frame = self.navigationBarDefaultFrame self.tabBarMovingDistance = 0 self.navigationBarMovingDistance = 0 self.updateConstraints() } /// TopとBottomの制約を更新する private func updateConstraints() { guard let superview = self.scrollView.superview else { return } if self.tabBar != nil { self.bottomConstraint?.constant = -superview.safeAreaInsets.bottom + self.tabBarMovingDistance } if self.navigationBar != nil { self.topConstraint?.constant = superview.safeAreaInsets.top + self.navigationBarMovingDistance } } }VC側
override func viewDidLoad() { super.viewDidLoad() self.hideBarManager.viewDidLoad() } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() self.hideBarManager.viewDidLayoutSubviews() } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) self.hideBarManager.viewWillDisappear() }UIScrollViewDelegateの処理
UIScrollViewDelegateの各メソッドが呼び出されるタイミングで実行する処理をHideBarManagerに実装していきます。
extension HideBarManager { func scrollViewShouldScrollToTop() { self.showBarImmediately() } /// ドラッグ開始 func scrollViewWillBeginDragging() { if self.scrollBeginningOffsetY == nil { self.scrollBeginningOffsetY = self.scrollView.contentOffset.y self.tabBarMovingDistance = 0 self.navigationBarMovingDistance = 0 } } /// ドラッグ終了 func scrollViewDidEndDragging(decelerate: Bool) { // 慣性によるスクロールがない場合は、スクロール開始位置とスクロール方向を初期化する if !decelerate { self.scrollBeginningOffsetY = nil self.scrollDirectionY = .none } } /// スクロール中 func scrollViewDidScroll() { guard self.scrollView.isDragging else { return } let scrollViewHeight = scrollView.frame.size.height let scrollContentSizeHeight = scrollView.contentSize.height let scrollOffset = scrollView.contentOffset.y let scrollBeginningOffset = self.scrollBeginningOffsetY ?? 0 // 一番上に到達した場合は、Barを表示する if scrollOffset <= 0 { self.showBarImmediately() return } // 一番下に到達した場合は、何もしない if scrollContentSizeHeight <= scrollOffset + scrollViewHeight { return } // 下へスクロールしていて、 // スクロール方向をまだ保持していない or 保持しているスクロール方向がbottomの場合 // ナビゲーションバーとタブバーを移動させる if scrollBeginningOffset < scrollOffset { if self.scrollDirectionY == .none || self.scrollDirectionY == .bottom { self.scrollDirectionY = .bottom self.moveTabBarAndNavigationBar() return } } // 上へスクロールしていて、 // スクロール方向をまだ保持していない or 保持しているスクロール方向がtopの場合 // ナビゲーションバーとタブバーを移動させる if scrollBeginningOffset > scrollOffset { if self.scrollDirectionY == .none || self.scrollDirectionY == .top { self.scrollDirectionY = .top self.moveTabBarAndNavigationBar() return } } } /// スクロール停止 func scrollViewDidEndDecelerating() { self.scrollBeginningOffsetY = nil self.scrollDirectionY = .none } } extension HideBarManager { /// タブバーとナビゲーションバーを移動させる private func moveTabBarAndNavigationBar() { guard let tabBarFrame = self.calcTabBarFrame(), let navigationBarFrame = self.calcNavigationBarFrame() else { return } self.tabBar?.frame = tabBarFrame self.navigationBar?.frame = navigationBarFrame self.tabBarMovingDistance = tabBarFrame.origin.y - self.tabBarDefaultFrame.origin.y self.navigationBarMovingDistance = navigationBarFrame.origin.y - self.navigationBarDefaultFrame.origin.y self.updateConstraints() } /// スクロール量からタブバーのoffsetを算出し、タブバーのframeを返す private func calcTabBarFrame() -> CGRect? { guard let scrollBeginningOffsetY = self.scrollBeginningOffsetY else { return nil } // 移動量 let movingDistance = self.scrollView.contentOffset.y - scrollBeginningOffsetY var newTabBarOriginY = self.tabBarDefaultFrame.origin.y + movingDistance // もとの位置より上には移動させない let min = self.tabBarDefaultFrame.origin.y if newTabBarOriginY < min { newTabBarOriginY = min } // 画面外に出たらそれ以上は下に移動させない let max = self.tabBarDefaultFrame.origin.y + (self.tabBarDefaultFrame.size.height * 2) if newTabBarOriginY > max { newTabBarOriginY = max } var newTabBarFrame = self.tabBarDefaultFrame! newTabBarFrame.origin.y = newTabBarOriginY return newTabBarFrame } /// スクロール量からナビゲーションバーのoffsetを算出し、ナビゲーションバーのframeを返す private func calcNavigationBarFrame() -> CGRect? { guard let scrollBeginningOffsetY = self.scrollBeginningOffsetY else { return nil } // 移動量 let movingDistance = self.scrollView.contentOffset.y - scrollBeginningOffsetY var newNavigationBarOriginY = self.navigationBarDefaultFrame.origin.y - movingDistance let statusBarHeight = self.scrollView.window?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0 // 画面外に出たらそれ以上は上に移動させない let min = self.navigationBarDefaultFrame.origin.y - (self.navigationBarDefaultFrame.size.height * 2) - statusBarHeight if newNavigationBarOriginY < min { newNavigationBarOriginY = min } // もとの位置より下には移動させない let max = self.navigationBarDefaultFrame.origin.y if newNavigationBarOriginY > self.navigationBarDefaultFrame.origin.y { newNavigationBarOriginY = max } var newNavigationBarFrame = self.navigationBarDefaultFrame! newNavigationBarFrame.origin.y = newNavigationBarOriginY return newNavigationBarFrame } }VC側
func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool { self.hideBarManager.scrollViewShouldScrollToTop() return true } func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { self.hideBarManager.scrollViewWillBeginDragging() } func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { self.hideBarManager.scrollViewDidEndDragging(decelerate: decelerate) } func scrollViewDidScroll(_ scrollView: UIScrollView) { self.hideBarManager.scrollViewDidScroll() } func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { self.hideBarManager.scrollViewDidEndDecelerating() }さいごに
今回実装したソースコード全体は、GitHubにpushしていますm(_ _)m
実装過程で、iOS13ではちゃんとNavigationBarが隠れたのにiOS14では同じロジックでちゃんと隠れてくれなかったりして
思いの外大変でした。。現状の実装では、非表示状態から再表示される時など見え方が微妙なところもまだまだあるので
もう少しきれいに表示/非表示切り替えられるようアプデしていきたいなーと思います|ω・`)明日の投稿もお楽しみに〜(・ω・)ノ
- 投稿日:2020-12-06T01:51:35+09:00
MetricKit 入門
この記事は、ZOZOテクノロジーズ #4AdventCalendar2020の記事です。
昨日は@r-tezukaさんの「GKE上でgcloudコマンドを叩く為のPodをDeployしたら詰まった話」の記事でした。
ぜひご覧ください!ZOZOテクノロジーズ iOSエンジニアの@ahiruです。
皆さんはアプリのパフォーマンス計測を行っていますか?
パフォーマンス計測はどのように行っていますか?
手法としては様々ありますが、近年Appleはこの分野に力を入れて取り組んでおりAppleの標準フレームワークであるMetricKitを使用するのも一つの手です。
Xcode11 / iOS13より登場したMetricKitはiOS14になって大きな進化を遂げましたが、おそらくあまり知られていない(?)と思いますので本稿でまとめていきます。
Xcode 11 と iOS 13 で追加された機能の振り返り
Xcode12時代に突入している今日ではありますが、新機能の説明の前に簡単に振り返りをします。
Xcode11 と iOS13からは metrics に関する3つのツールが導入されました。
XCTest Metrics
XCTest から直接メトリックスの収集を行えるようになりました。
XCTPerformanceMetric というパフォーマンス計測のための metrics は Xcode9 から取得可能でしたが、Xcode11 からは XCTMetric という protocol が追加され、下記 function によって cpu やメモリに関する metrics が取得できるようになりました。
func measure(metrics: [XCTMetric], block: () -> Void)XCTest Metrics は特定の機能の影響を見極めるのに有効です。
MetricKit
アプリユーザーの使用状況(メトリックス)の収集を行うためのフレームワークです。
概要に関してはこちらの記事を読んでみてください!去年のアドベントカレンダーで書きました、、
端的に説明すると下記のようになります。
MetricKitを使用すると、オンデバイスのアプリ診断や、システムがキャプチャした電力とパフォーマンスのメトリックスを受け取ることができます。登録されたアプリは、最大でも 1 日 1 回、過去 24 時間分のデータを含むレポートを受信します。
レポートのデータを使用して、アプリのパフォーマンスを向上させることができます。なんと言っても、アプリユーザーの環境での使用状況を詳細に把握することができるというのが最大のメリットです。
実装方法については後ほどご紹介いたします。
Metrics Organizer
みなさんご存知の機能かと思いますが、ノンコードでOrgnizerから集約されたデータの確認ができるようになりました。
MetricKit の実装方法
import MetricKit @main class AppDelegate: UIResponder, UIApplicationDelgate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool) { MXMetricManager.shared.add(self) return true } func applicationWillTerminate(_ application: UIApplication) { MXMetricManager.shared.remove(self) } } extension AppDelegate: MXMetricManagerSubscriber { func didReceive(_ payload: [MXMetricPayload]) { // データを自社サーバーへ送信するなど } }基本的にたったこれだけで実装完了です。
1. MetricKitをimport
2.
MXMetricManager.shared.addをアプリ起動時に実装3.
MXMetricManager.shared.removeをteminate時に実装4.
didReceiveで metrics のデータを取得実際に動かしてみると様々なmetricsが取得できます。(実機でのみテストが可能)
iOS14からの MetricKit の新機能
ようやく本題です。
iOS14 で MetricKit は進化を遂げ、2つのアップデートがありました。
1. 取得できる metrics の種類が増えた
1つ目は取得できる metrics の種類が増えました。
増えたのは、「CPU命令数」「スクロールヒッチ」「アプリケーションの終了理由」です。CPU命令数
MXCPUMetricは CPU の使用に関するメトリックを表すクラスです。
iOS13では使用したCPUの合計量のみが取得可能でしたが、iOS14からはCPUの命令総数が取得できるようになりました。
これの何が嬉しいかですが、CPU命令数はハードウェアや周波数に依存しないアプリケーションが行った作業の絶対的なメトリックと言えるのでアプリケーションの合計負荷をより正確に数量化できるようになります。
スクロールヒッチ
MXAnimationMetric はアプリ内のアニメーションの応答性に関する指標を表すクラスです。スクロールの中ヒッチングに費やした時間の比率を取得できるようになったのですが、スクロールヒッチに関する詳しい説明はWWDC2020で発表された Eliminate animation hitches with XCTest をご覧ください!
アプリケーションの終了理由
MXAppExitMetric はフォアグラウンドとバックグラウンドでのアプリの終了(kill)原因に関するメトリックを表すクラスです。つまりアプリがなぜkillされたのか、その原因調査をすることがかなり容易になりました。
これはかなり大きなアップデートだと感じています。この部分に関しては、WWDC2020のセッションの一つである Why is my app getting killed? をぜひみてください!
2. MetricKit診断
MetricKit Diagnostic(メトリックキット診断)と呼ばれるものが追加されました。
iOS13では純粋に metrics を取得するだけだった MetricKit ですが、iOS14からは不具合の原因特定に役立つような情報も取得できるようになりました。
iOS13とiOS14を比較するとこんな感じです。
iOS13
iOS14
metricsだけではなくクラッシュに関する情報なども取得できるようになります。クラッシュに関してはcrashlyticsなどサードパーティ製ツールを使用することがほとんどかと思いますが、これがApple純正のフレームワークに置き換わる未来があるかもしれません。
まとめ
- MetricKitはベータ版 / リリース版のアプリのパフォーマンスについてユーザーの環境ベースで様々なメトリックスを収集するためのフレームワーク
- (やりたいことにもよると思うが)基本的に実装は簡単
- 収集したメトリックスは自社サーバーやサードパーティ製ツールへ送信・蓄積して分析を行いアプリのパフォーマンス改善に役立てる(パフォーマンスのデグレに効果的)
- MetricKitはiOS14で進化しており今後主流となる可能性を秘めていると感じたが、現状は情報が極端に少なく実装方法など調べるのも一苦労
- 投稿日:2020-12-06T00:38:53+09:00
競プロで使える関数一覧(Swift)
はじめに
本記事は Swift/Kotlin愛好会 Advent Calendar 2020 の6日目の記事です。
競プロで使える関数を紹介します。注意
- 本記事で紹介している関数および計算量が正しいかは保証できていません。
- さらに計算量が小さいプログラムが存在する可能性があります。
- 本記事は随時更新予定です。
環境
- OS:macOS Big Sur 11.0.1
- Xcode:12.2 (12B45b)
- Swift:5.3.1
5.2.1(2020/12/06現在のAtCoder)でも動作する競プロで使える関数一覧
順列全探索
説明
TBD
ソースコード
private func permutations(of values: [Int]) -> [[Int]] { if values.count <= 1 { return [values] } var results: [[Int]] = [] for i in values.indices { let baseValue = values[i] var excludingBaseValue = values excludingBaseValue.remove(at: i) results += permutations(of: excludingBaseValue).map { [baseValue] + $0 } } return results }
出力例
[1, 3] -> [[1, 3], [3, 1]] [1, 2, 4] -> [[1, 2, 4], [1, 4, 2], [2, 1, 4], [2, 4, 1], [4, 1, 2], [4, 2, 1]] [1, 2, 3, 5] -> [[1, 2, 3, 5], [1, 2, 5, 3], [1, 3, 2, 5], [1, 3, 5, 2], [1, 5, 2, 3], [1, 5, 3, 2], [2, 1, 3, 5], [2, 1, 5, 3], [2, 3, 1, 5], [2, 3, 5, 1], [2, 5, 1, 3], [2, 5, 3, 1], [3, 1, 2, 5], [3, 1, 5, 2], [3, 2, 1, 5], [3, 2, 5, 1], [3, 5, 1, 2], [3, 5, 2, 1], [5, 1, 2, 3], [5, 1, 3, 2], [5, 2, 1, 3], [5, 2, 3, 1], [5, 3, 1, 2], [5, 3, 2, 1]]
計算量
TBD
参考リンク
TBD
約数列挙
説明
与えられた数値の約数をすべて求め、昇順に整列した配列で返します。
整列する必要がない場合、返却時のsorted()を外してください。
ソースコード
import Foundation private func divisors(of value: Int) -> [Int] { var divisors: Set<Int> = [] for i in 1...(Int(floor(sqrt(Double(value))))) where value % i == 0 { divisors.insert(i) divisors.insert(value / i) } return divisors.sorted() }
divisors変数を通常の配列でなくSet<Int>型で定義している理由は、数値が平方数(例: 25)の場合に約数が重複するのを防ぐためです。
通常の配列は重複を許すため25 -> [1, 5, 5, 25]となりますが、Set<Int>は重複を許さないので25 -> [1, 5, 25]となります。
出力例
1 -> [1] 2 -> [1, 2] 3 -> [1, 3] 4 -> [1, 2, 4] 5 -> [1, 5] 6 -> [1, 2, 3, 6] 7 -> [1, 7] 8 -> [1, 2, 4, 8] 9 -> [1, 3, 9] 10 -> [1, 2, 5, 10] 11 -> [1, 11] 12 -> [1, 2, 3, 4, 6, 12] 13 -> [1, 13] 14 -> [1, 2, 7, 14] 15 -> [1, 3, 5, 15] 16 -> [1, 2, 4, 8, 16] 17 -> [1, 17] 18 -> [1, 2, 3, 6, 9, 18] 19 -> [1, 19] 20 -> [1, 2, 4, 5, 10, 20] 21 -> [1, 3, 7, 21] 22 -> [1, 2, 11, 22] 23 -> [1, 23] 24 -> [1, 2, 3, 4, 6, 8, 12, 24] 25 -> [1, 5, 25] 26 -> [1, 2, 13, 26] 27 -> [1, 3, 9, 27] 28 -> [1, 2, 4, 7, 14, 28] 29 -> [1, 29] 30 -> [1, 2, 3, 5, 6, 10, 15, 30]
計算量
TBD
参考リンク
TBD
素因数分解
説明
与えられた数値を素因数分解し、昇順に整列した配列で返します。
ソースコード
import Foundation private func primeFactorizations(of value: Int) -> [Int] { precondition(value > 0) if (1...3).contains(value) { return [value] } var copiedValue = value var primeFactorizations: [Int] = [] for i in 2...Int(floor(Double(n) / 2)) { while copiedValue % i == 0 { copiedValue /= i primeFactorizations.append(i) } if primeFactorizations.reduce(1, *) == value { break } } if primeFactorizations.isEmpty { primeFactorizations.append(value) } return primeFactorizations }
出力例
1 -> [1] 2 -> [2] 3 -> [3] 4 -> [2, 2] 5 -> [5] 6 -> [2, 3] 7 -> [7] 8 -> [2, 2, 2] 9 -> [3, 3] 10 -> [2, 5] 11 -> [11] 12 -> [2, 2, 3] 13 -> [13] 14 -> [2, 7] 15 -> [3, 5] 16 -> [2, 2, 2, 2] 17 -> [17] 18 -> [2, 3, 3] 19 -> [19] 20 -> [2, 2, 5] 21 -> [3, 7] 22 -> [2, 11] 23 -> [23] 24 -> [2, 2, 2, 3] 25 -> [5, 5] 26 -> [2, 13] 27 -> [3, 3, 3] 28 -> [2, 2, 7] 29 -> [29] 30 -> [2, 3, 5]
計算量
TBD
参考リンク
TBD
最大公約数
TBD
最小公倍数
TBD
おわりに
以上、 Swift/Kotlin愛好会 Advent Calendar 2020 の6日目の記事でした。
明日も @uhooi の記事です。参考リンク
- uhooi/Programming-Swift-Solving: Solve competition programming using Swift.
- Swift版 競プロ用チートシート(初心者向け) - Qiita
まずは自力で解きたいため、こちらの記事はまだほとんど参考にしていない
- 投稿日:2020-12-06T00:20:06+09:00
TableViewセルの作り方 その2(Xib)
環境
Xcode 12.2
1.Xibファイルの作成
- Project navigatorでNew File...
- User InterfaceのViewを選択してNext → Xib名を入力してCreate
2.TableViewCellの作成
- デフォルトのViewを消す(選択してdelete)
- (+)をクリックしてLibraryからTableViewCellを選択しXibファイルにドラッグアンドドロップ
3. UITableViewCell クラスの作成
仮にSecondTableViewCellというUITableViewCell クラスにした場合
SecondTableViewCell.swiftclass SecondTableViewCell :UITableViewCell{ override func awakeFromNib() { super.awakeFromNib() } override func setSelected(_ selected: Bool, animated: Bool) { super.setSelected(selected, animated: animated) }4. XibとUITableViewClassの紐付け
- 使用するUITableViewCellクラスの設定
5. CellIdの設定
後でTableViewに登録する際に使う
6. UIをUITableViewCellクラスに紐付ける(ついでにUIのプロパティーも変更)
SecondTableViewCell.swiftclass SecondTableViewCell :UITableViewCell{ //★★★UIとの接続 @IBOutlet weak var cellImageView: UIImageView! @IBOutlet weak var textView: UITextView! override func awakeFromNib() { super.awakeFromNib() //CellのUIのプロパティー変更 cellImageView.layer.cornerRadius = 25 } override func setSelected(_ selected: Bool, animated: Bool) { super.setSelected(selected, animated: animated) } }7. TableViewへのセルの登録
viewDidLoad内などに記述
private let cellId = "cellId" @IBOutlet weak var secondTableView: UITableView! override func viewDidLoad() { super.viewDidLoad() //セルの登録(Xibファイル名:SecondViewTableViewCell.xibの場合) secondTableView.register(UINib(nibName: "SecondViewTableViewCell", bundle: nil), forCellReuseIdentifier: cellId) } }出来上がり
全コード
SecondTableViewCell.swiftimport UIKit class SecondTableViewCell :UITableViewCell{ //cellIdを決めて格納 private let cellId = "cellId" //★★★UIとの接続 @IBOutlet weak var cellImageView: UIImageView! @IBOutlet weak var textView: UITextView! override func awakeFromNib() { super.awakeFromNib() //CellのUIのプロパティー変更 cellImageView.layer.cornerRadius = 25 } override func setSelected(_ selected: Bool, animated: Bool) { super.setSelected(selected, animated: animated) } }SecondViewController.swiftimport UIKit class SecondViewController : UIViewController{ private let cellId = "cellId" @IBOutlet weak var secondTableView: UITableView! override func viewDidLoad() { super.viewDidLoad() secondTableView.delegate = self secondTableView.dataSource = self //セルの登録(Xibファイル名:SecondViewTableViewCell.xibの場合) secondTableView.register(UINib(nibName: "SecondViewTableViewCell", bundle: nil), forCellReuseIdentifier: cellId) } } extension SecondViewController :UITableViewDelegate,UITableViewDataSource{ //セルの高さを指定 func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return 100 } //セルの個数 func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 10 } //セルの内容 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = secondTableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath) cell.backgroundColor = .lightText return cell } }最後に
私の理解が間違っていたらご指摘いただければ幸いです。
- 投稿日:2020-12-06T00:07:08+09:00
ネイティブから逃げるな。Pigeonを使ったタイプセーフなFlutter + ネイティブ開発
はじめに
この記事はFlutterAdventカレンダー2020#1 の6日目の記事です。
こんにちは。@glassmonkeyです。
最近サボりがちですがたまにFlutterのことなどをつぶやいてたりします。
仕事ではサーバーサイドエンジニアをやっています。普段はPHP,Go,Pythonあたりを書くことが多いですが、趣味でFlutterアプリ開発をしています。今回はFlutterの1.20リリースで発表されたPigeonを使ったネイティブ連携をやってみたので、そのご紹介になります。
ネイティブ開発はほぼ素人です。Swiftは業務で3ヶ月ほど、Kotlinは今回初めてまともに書きましたが
そこまで苦労せずに書けました。これからネイティブのプラグインを作らないといけないと思ってる方々などの助けになれば幸いです。
要約
- Pigeonは静的ジェネレータだよ
- 型ファイルからネイティブとdartファイルを生成するので、各種補完が効くようになるよ
- バージョン1未満だからこれからが楽しみだね
サンプルコードについて
この記事で紹介したプログラムに関してはglassmonkey/flutter_wifiにアップしているので、もしよければご覧ください。
今回用意したサンプルは、このような感じで
Wifiの接続状態(Wifi/モバイル通信/接続なし)をネイティブからコールバックで受け取って表示を切り替える簡単なものです。動画はAndroidのものですが、iOSも同様です。
- ボタンを押すと現在の接続状態がわかる ボタンを押して検知モード(ボタンが緑色)の状態になると接続状態を表示します。
- Wifi Connection(Wifi接続中のとき)
- Mobile Connection(いわゆるキャリア通信のとき)
- Lost Connection(機内モードや圏外のとき)
- 接続状態がわかる状態だと現在の接続状態が変更すると自動で表示が切り替わる
例だとクイックメニューでWifiをOFFにするとキャリア通信に切り替えたので、
Wifi ConnectionからMobile Connectionに表示が切り替わっています。各種バージョンについて
執筆にあたっての筆者の各バージョンはこのような感じです
$ flutter doctor [✓] Flutter (Channel stable, 1.22.3, on Mac OS X 10.15.7 19H15, locale ja-JP) [✓] Android toolchain - develop for Android devices (Android SDK version 29.0.2) [✓] Xcode - develop for iOS and macOS (Xcode 12.2) [✓] Android Studio (version 4.0)また、サンプルアプリの実機確認では以下のバージョンの両OSで確認を取りました。
iOSに関してはシミュレータで接続状態のシミューレートの確認ができなかったので実機で動作させることをおすすめします。
Anrdroid: 9 iOS: 14.1Androidに関しては
minSdkVersion: 21としたので、実機で確認する場合はOSのバージョンなどご注意ください。Flutterとネイティブ連携について
私自身ほとんどネイティブ機能に関して書いた経験がないのですが公式
に詳しく記載されているので詳細はそちらをご覧いただけたらと思います。基本的な構成は以下の図の通りです。
MethodCnannelと呼ばれる通信経路を用意して、ネイティブとFlutterをやりとりする形となります。
これは後術するPigeonを使った方法でも代わりません。FlutterとAndroidの実装例を示します。
platform-integration/platform-channelsより引用しました。1. メソッドチャンネルを用意する
Flutter側のmethodチャンネルの呼び出しをします。
final platform = const MethodChannel('samples.flutter.dev/battery');ネイティブ側の呼び出しを登録します。
class MainActivity: FlutterActivity() { private val CHANNEL = "samples.flutter.dev/battery" override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result -> // Note: this method is invoked on the main thread. // TODO } } }慣例としてメソッドチャンネル名はパッケージ名を使って、他のアプリとの衝突を防ぐようにしておくようです。
2. メソッドチャンネルからメソッドを呼び出す
final int result = await platform.invokeMethod('getBatteryLevel');ネイティブ側の実装は以下の感じです
private fun getBatteryLevel(): Int { val batteryLevel: Int if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { val batteryManager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY) } else { val intent = ContextWrapper(applicationContext).registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED)) batteryLevel = intent!!.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100 / intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1) } return batteryLevel }この実装でも悪くはないのですが、メソッド呼び出しが
result = await platform.invokeMethod('getBatteryLevel');といった形式になるので、コンパイルセーフではないといった問題があげられます。補完も効かないですしね。
連携するメソッドが増えてくれば結構きつくなってきそうかなと思います。
Pigeonとは
そこで、従来の補完が効かないといった問題の対処に開発されたのが、Pigeonとよばれるツールです。
Flutter1.20で公式で公開された、
Flutterとホストプラットフォーム間の通信をタイプセーフで簡単にするコードジェネレータツールです。生成もとのスキーマファイルとなるDartファイルを用意しておき、
iOSのためのObjective-CとAndroid用のJavaを生成します。うげっと思われる方もいらっしゃるかもしれませんが、SwiftからObject-Cのコードは呼べますし、
KotlinからもJavaのコードは呼べるので実際はそんなに気にならなかったりはします。データのやりとりはいわゆるスキーマファイルから各環境共通のApi定義をしてFlutterをクライアント、
ネイティブをサーバーとして、サバクラなやりとりをします。まだまだバージョン1にもなっていないですが、issueがたくさんあるのでこれからに期待したいですね
https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+pigeonスキーマファイルについて
以下のような感じで書きます。
ファイル生成は
--inputでスキーマファイルを指定して実行できます。
スキーマファイルのパスは自由に決めちゃってください。lib配下でなくてもOKです。$ flutter pub run pigeon --input lib/pigeon/scheme.dartScheme.dart
import 'package:pigeon/pigeon.dart'; // 引数の定義 class BatteryRequest { String unit; } // 戻り値の定義 class BatteryResponse { String responseMessage; } @HostApi() abstract class BatteryApi { BatteryResponse call(BatteryRequest req); } // 戻り値の定義 class WifiResponse { bool availableWifi; bool availableMobile; } @HostApi() abstract class WifiApi { WifiResponse call(); } @FlutterApi() abstract class WifiCallbackApi { void apply(WifiResponse response); } // 生成されるファイルの出力先などの設定 void configurePigeon(PigeonOptions opts) { opts.dartOut = 'lib/native/api.dart'; opts.javaOut = 'android/app/src/main/java/nagano/shunsuke/plugins/Pigeon.java'; opts.javaOptions.package = "nagano.shunsuke.plugins"; opts.objcHeaderOut = 'ios/Runner/Pigeon.h'; opts.objcSourceOut = 'ios/Runner/Pigeon.m'; opts.objcOptions.prefix = 'Flutter'; //nnbdしたいときはこれをONにする //opts.dartOptions.isNullSafe = true; }このスキーマファイルで重要な点は3点あります。
Apiを定義する
データをやりとりするApiになります。
ネイティブ側で機能を実装するう@HostApiとFlutter側で実装をする@FlutterApiが用意されています。Apiでやりとりするためのデータ構造を決定する。
ネイティブ Flutter間でデータをやりとりするための、
いわゆるリクエストとレスポンス用のデータ構造を決めます。configurePigeonで出力設定を決めます。
生成コマンドラインオプションからも設定できたりはするので、ここで設定するかはわりと好みですね。
ここで設定した内容はコマンドラインオプションよりも優先度はあったりします。Apiについて
@HostApiと@FlutterApiの使い分けや例を説明します。
ネイティブとの細かい連携に関しては後術します。HostApiについて
前者の
@HostApiはネイティブ側でApi実装を書く場合が該当します。
Flutterからネイティブ側の機能を呼び出したいときに便利です。FlutterApiについて
後者の
@FlutterApiはFlutter側でApi実装を書く場合が該当します。
ネイティブからFlutterを呼び出すときに利用するとよさげで、コールバック実装やネイティブ側のイベントハンドラから呼び出すときに便利です。Apiでやりとりするためのデータ構造をきめる。
この記事を執筆時点だと以下の型がサポートされているので、それを組み合わせたデータ構造となります。それぞれJavaとObject-Cで対応するデータ構造に変換されます。
const List<String> _validTypes = <String>[ 'String', 'bool', 'int', 'double', 'Uint8List', 'Int32List', 'Int64List', 'Float64List', 'List', 'Map', ];https://github.com/flutter/packages/blob/master/packages/pigeon/lib/pigeon_lib.dart#L18-L27
configurePigeonで出力設定を決める
以下の設定は設定しておくと良いでしょう。パッケージ名などは適宜読み替えてください。
void configurePigeon(PigeonOptions opts) { opts.dartOut = 'lib/native/api.dart'; opts.javaOut = 'android/app/src/main/java/nagano/shunsuke/plugins/Pigeon.java'; opts.javaOptions.package = "nagano.shunsuke.plugins"; opts.objcHeaderOut = 'ios/Runner/Pigeon.h'; opts.objcSourceOut = 'ios/Runner/Pigeon.m'; opts.objcOptions.prefix = 'Flutter'; //nnbdしたいときはこれをONにする //opts.dartOptions.isNullSafe = true; }
opts.dartOutは生成するdartファイル名パスを指定します。Flutter側で利用します。opts.javaOutは生成するJavaファイル名パスを指定します。Android側で利用します。opts.javaOptions.packageは生成するファイルのパッケージ名です。生成するJavaファイルとのパスとの兼ね合いに注意しましょう。
opts.objcHeaderOutはiOSで利用する。Object−C用のヘッダーファイル名パスです。生成したファイルをXcode側でAddすることを忘れないようにしましょう。Runner-Bridging-Header.hに読み込ませて利用します。
opts.objcSourceOutはiOSで利用する。Object−Cファイルの実装です。こちらもヘッダーファイルと同様にXcode側でAddすることを忘れないようにしましょう。
opts.objcOptions.prefixで付与された文字列はiOS側で呼び出す際のPrefixとなります。
opts.dartOptions.isNullSafeをするとFlutter側で生成されるコードがNNBD準拠になります。今回はサンプルコードなので無効にしました。開発フロー
ではサンプルアプリのコードを交えて、実際にネイティブとFlutter側の開発フローの説明をします。
このサンプルアプリ特有うの接続状態を取得するために追加ライブラリを入れるフローも入れてたりも含めているので、
適宜読み替えていただけたらと思います。iOS, Android共通設定
1. Pigeonを使うための準備をする
準備といってもライブラリを入れるだけです。
(Android・iOSの両ビルドができる前提ではあります)基本的に生成用に使用するだけで、プロダクションコードからは呼び出さないものなので、
pubspec.ymlのdev_dependenciesに追記します。dev_dependencies: pigeon: ^0.1.152. 実行コマンドを叩いてファイル生成をする
今回は実行コマンドのラッパーシェルを用意したのでtools/pigeon.shを用意したのでそれを叩きます。
$ ./tools/pigeon.sh内容は大したことないですが、android側の生成先ディレクトリが存在しない場合にエラーとなることがあったので、
ディレクトリを追加を行っています。tools/pigeon.sh#!/bin/bash cd "$(PWD)" mkdir -p android/app/src/main/java/nagano/shunsuke/plugins/ flutter pub run pigeon --input lib/pigeon/scheme.dart3. 生成されたファイルを確認する
scheme.dartに記載されている以下の内容に応じてファイルが生成されています。
AndroidとiOS側の生成ファイルに関してはそれぞれのOS導入の説明で詳しく触れます。
// 生成されるファイルの出力先などの設定 void configurePigeon(PigeonOptions opts) { opts.dartOut = 'lib/native/api.dart'; opts.javaOut = 'android/app/src/main/java/nagano/shunsuke/plugins/Pigeon.java'; opts.javaOptions.package = "nagano.shunsuke.plugins"; opts.objcHeaderOut = 'ios/Runner/Pigeon.h'; opts.objcSourceOut = 'ios/Runner/Pigeon.m'; opts.objcOptions.prefix = 'Flutter'; //nnbdしたいときはこれをONにする //opts.dartOptions.isNullSafe = true; }
- Flutter側生成ファイル:
lib/native/api.dartAndroid側生成ファイル:
android/app/src/main/java/nagano/shunsuke/plugins/Pigeon.javaios側生成ファイル:
ios/Runner/Pigeon.hおよびios/Runner/Pigeon.mFlutter用の生成ファイルについての解説
https://github.com/glassmonkey/flutter_wifi/blob/master/lib/native/api.dart
// Autogenerated from Pigeon (v0.1.15), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import // @dart = 2.8 import 'dart:async'; import 'package:flutter/services.dart'; import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; class WifiResponse { bool availableDetect; bool availableWifi; bool availableMobile; // ignore: unused_element Map<dynamic, dynamic> _toMap() { final Map<dynamic, dynamic> pigeonMap = <dynamic, dynamic>{}; pigeonMap['availableDetect'] = availableDetect; pigeonMap['availableWifi'] = availableWifi; pigeonMap['availableMobile'] = availableMobile; return pigeonMap; } // ignore: unused_element static WifiResponse _fromMap(Map<dynamic, dynamic> pigeonMap) { final WifiResponse result = WifiResponse(); result.availableDetect = pigeonMap['availableDetect']; result.availableWifi = pigeonMap['availableWifi']; result.availableMobile = pigeonMap['availableMobile']; return result; } } class WifiRequest { bool isDetect; // ignore: unused_element Map<dynamic, dynamic> _toMap() { final Map<dynamic, dynamic> pigeonMap = <dynamic, dynamic>{}; pigeonMap['isDetect'] = isDetect; return pigeonMap; } // ignore: unused_element static WifiRequest _fromMap(Map<dynamic, dynamic> pigeonMap) { final WifiRequest result = WifiRequest(); result.isDetect = pigeonMap['isDetect']; return result; } } abstract class CallbackApi { void apply(WifiResponse arg); static void setup(CallbackApi api) { { const BasicMessageChannel<dynamic> channel = BasicMessageChannel<dynamic>('dev.flutter.pigeon.CallbackApi.apply', StandardMessageCodec()); if (api == null) { channel.setMessageHandler(null); } else { channel.setMessageHandler((dynamic message) async { final Map<dynamic, dynamic> mapMessage = message as Map<dynamic, dynamic>; final WifiResponse input = WifiResponse._fromMap(mapMessage); api.apply(input); }); } } } } class Api { Future<WifiResponse> call(WifiRequest arg) async { final Map<dynamic, dynamic> requestMap = arg._toMap(); const BasicMessageChannel<dynamic> channel = BasicMessageChannel<dynamic>('dev.flutter.pigeon.Api.call', StandardMessageCodec()); final Map<dynamic, dynamic> replyMap = await channel.send(requestMap); if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', details: null); } else if (replyMap['error'] != null) { final Map<dynamic, dynamic> error = replyMap['error']; throw PlatformException( code: error['code'], message: error['message'], details: error['details']); } else { return WifiResponse._fromMap(replyMap['result']); } } }生成されたコードの中身をみてみると内部的には
BasicMessageChannelを使ってるだけだったりします。const BasicMessageChannel<dynamic> channel = BasicMessageChannel<dynamic>('dev.flutter.pigeon.CallbackApi.apply', StandardMessageCodec());内部では勝手に
dev.flutter.pigeon以下の名前でやりとりをするようなので、実際で扱う場合はApi名にアプリケーション名をPrefixなりをつけたりして名前衝突を避けた方が良さそうな気はしますね。従来のPlatformChannelでメソッド名の文字列の対応を気にしながら開発していたときに比べると格段に扱いやすくなったと思いました。
基本的にデータのやりとりを行うためのオブジェクト(今回の例だと
WifiRequestとWifiResponse)に関してはイミュータブルな感じではなかったりします。ですので、Flutter側とネイティブ間のやりとりをするためのDTOと割り切って使いアプリ全体としては依存度を下げて使うのが行儀良さそうに思います。また、前述の
FlutterApiとHostApiの違いがここに現れてきます。
FlutterApiの場合だとabstractキーワードがついたままで追加実装をFlutter側で行わないといけないことがわかります。abstract class CallbackApi { void apply(WifiResponse arg); static void setup(CallbackApi api) { { const BasicMessageChannel<dynamic> channel = BasicMessageChannel<dynamic>('dev.flutter.pigeon.CallbackApi.apply', StandardMessageCodec()); if (api == null) { channel.setMessageHandler(null); } else { channel.setMessageHandler((dynamic message) async { final Map<dynamic, dynamic> mapMessage = message as Map<dynamic, dynamic>; final WifiResponse input = WifiResponse._fromMap(mapMessage); api.apply(input); }); } } } }今回だとFlutter側で呼び出したcallback関数を呼び出すようにしたいので、
このような形で追加実装しましたhttps://github.com/glassmonkey/flutter_wifi/blob/master/lib/native.dart#L69-L79
class CallbackApiImpl extends CallbackApi { final Function(WifiResponse response) caller; CallbackApiImpl(this.caller); @override void apply(WifiResponse response) { print(response); this.caller(response); } }また、initStateなどの初期化タイミングでabstract classの静的な
setupメソッドを呼び実装を登録します。
その際にコールバック関数changeConnectionの登録も同時に済ましてしまいます。今回はサンプルなので、Setestate経由の状態変更で楽に実装したかったので、Statefulウィジェット初期化時に呼び出す形式としています。しかし、実際はProviderを使うと思うので配る前に初期化しておくと良いでしょう。
https://github.com/glassmonkey/flutter_wifi/blob/master/lib/native.dart#L24
CallbackApi.setup(CallbackApiImpl(this.changeConnection));コールバック関数として渡している
changeConnectionは
wifiの状態オブジェクト(WifiResponse response)を受け取ってsetStateするだけのメソッドです。https://github.com/glassmonkey/flutter_wifi/blob/master/lib/native.dart#L51-L67
void changeConnection(WifiResponse response) { setState(() { if (!response.availableDetect) { _wifiText = "Stop Detection"; return; } if (response.availableWifi) { _wifiText = "WIFI Connection"; } else if (response.availableMobile) { _wifiText = "Mobile Connection"; } else { _wifiText = "Lost Connection"; } }); } }一方で
HostApiの場合だと、abstractキーワードが外れているのでFlutter側はこれを使えばいいだけということがわかります。class Api { Future<WifiResponse> call(WifiRequest arg) async { final Map<dynamic, dynamic> requestMap = arg._toMap(); const BasicMessageChannel<dynamic> channel = BasicMessageChannel<dynamic>('dev.flutter.pigeon.Api.call', StandardMessageCodec()); final Map<dynamic, dynamic> replyMap = await channel.send(requestMap); if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', details: null); } else if (replyMap['error'] != null) { final Map<dynamic, dynamic> error = replyMap['error']; throw PlatformException( code: error['code'], message: error['message'], details: error['details']); } else { return WifiResponse._fromMap(replyMap['result']); } } }今回利用している箇所ではボタン押下時に、同期的にWifiの状態をネイティブに問い合わせする動作に利用しています。
https://github.com/glassmonkey/flutter_wifi/blob/master/lib/native.dart#L41-L49
Future<void> fetchData() async { setState(() { _isSwitch = !_isSwitch; }); final api = Api(); final request = WifiRequest(); request.isDetect = _isSwitch; this.changeConnection(await api.call(request)); }以上で基本的なFlutter側の設定は完了です。
Android側の導入設定
Androidは基本的にすんなり実装できた気がしたので、まずはAndroidから手を付けることをおすすめします。
Android側の作業を行う際は
Tools>Flutter>Open for Editing in Android Studioから別窓でプロジェクトとして開いておくとよいでしょう。各種補完が効くので自分みたいなkotlin素人でも楽にできたのでおすすめです。
1. パッケージと生成パスを設定する
今回はpackageを
nagano.shunsuke.pluginsとしたので、それに対応するディレクトリを作成しておきます。
今回は静的ファイル生成時のtools/pigeon.shでディレクトリ生成をさせていいます。
このときにscheme.dart上のconfigurePigeonのopts.javaOutとopts.javaOptions.package
の対応を忘れないようにしておきましょう。void configurePigeon(PigeonOptions opts) { opts.dartOut = 'lib/native/api.dart'; opts.javaOut = 'android/app/src/main/java/nagano/shunsuke/plugins/Pigeon.java'; opts.javaOptions.package = "nagano.shunsuke.plugins"; opts.objcHeaderOut = 'ios/Runner/Pigeon.h'; opts.objcSourceOut = 'ios/Runner/Pigeon.m'; opts.objcOptions.prefix = 'Flutter'; //nnbdしたいときはこれをONにする //opts.dartOptions.isNullSafe = true; }2.ビルド設定の変更
Pigeonそのものには関係ないですが、Wifiの情報取得の方法がsdkのバージョンで異なっていたので、最新の方法だけの対応としました。そのためminSdkを
16から21に変更しています。minSdkVersion 21https://github.com/glassmonkey/flutter_wifi/blob/master/android/app/build.gradle#L45
3.権限の追加
manifestにwifiのアクセス関係の権限を追加します。
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />4. Apiの実装をする
生成されたファイルにインタフェースなどの定義内容が記載されているのでそれに沿って実装します。
定義関係が用意されていることで、Android側の開発でもコード補完が効くようになるわけです。生成されたファイルはJavaですが、実装そのものははkotlinからでも可能です。
今回はPigeon.Apiを実装します。class WifiApi(private val connectivityManager: ConnectivityManager, private val binaryMessenger: BinaryMessenger) : Pigeon.Api { var isWifi = false; var isMobile = false; val callback = Pigeon.CallbackApi(binaryMessenger) val mainHandler = Handler(Looper.getMainLooper()) private val networkCallback = object : ConnectivityManager.NetworkCallback() { override fun onAvailable(network: Network?) { super.onAvailable(network) // ネットワークが使用可能になったときの処理 val capabilities = connectivityManager.getNetworkCapabilities(network) isWifi = capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ?: false isMobile = capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ?: false mainHandler.post { val response = getWifiInfo() callback.apply(response) {} } } override fun onLost(network: Network?) { super.onLost(network) val capabilities = connectivityManager.getNetworkCapabilities(network) isWifi = capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ?: false isMobile = capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ?: false // ネットワークの接続が切れたときの処理 mainHandler.post { val response = getWifiInfo() callback.apply(response) {} } } override fun onUnavailable() { super.onUnavailable() isWifi = false isMobile = false mainHandler.post { val response = getWifiInfo() callback.apply(response) {} } } } private fun subscribe() { val request = NetworkRequest .Builder() .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) .build() connectivityManager.requestNetwork(request, networkCallback) } fun unSubscribe() { connectivityManager.unregisterNetworkCallback(networkCallback) } fun getWifiInfo(isDetect: Boolean = true): Pigeon.WifiResponse { val response = Pigeon.WifiResponse() response.availableWifi = isWifi response.availableMobile = isMobile response.availableDetect = isDetect return response; } override fun call(request: Pigeon.WifiRequest): Pigeon.WifiResponse { return if (request.isDetect) { subscribe() getWifiInfo() } else { unSubscribe() getWifiInfo(isDetect = false) } } }基本的な内容としてはFlutter側から送られてくるrequestのフラグに応じて、Wifiの状態をサブスクライブするかどうかを切り替えているだけです。
override fun call(request: Pigeon.WifiRequest): Pigeon.WifiResponse { return if (request.isDetect) { subscribe() getWifiInfo() } else { unSubscribe() getWifiInfo(isDetect = false) } }また、サブスクライブ中に変化した状態を通知する必要があるのですが、BinaryMessenger
が必要なので忘れないようにしましょう。iOSでも同様の作業が必要です。
kotlinの場合はエラーになるので気づけますが、swiftからobjectCのコードを呼ぶ場合にコンパイル通ってしまうので注意です。val callback = Pigeon.CallbackApi(binaryMessenger)また、コールバックを叩くときはメインスレッドで呼び出すことも留意しておきましょう。
Flutterがシングルスレッドでkotlin側からすると、Mainスレッドで動いているためです。
詳しきは公式のAndroindのためのFlutter解説のAsync UIを見ると良いでしょう。mainHandler.post { val response = getWifiInfo() callback.apply(response) {} }5. APIを登録する
FlutterActivityのconfigureFlutterEngine中でPigeon.Api.setupで登録すれば完了です。
今回だと実装したWifiApiを登録しています。
このレイヤーでいい感じでDIするようにしておけば、testableなコードに出来そうですね。override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager Pigeon.Api.setup(flutterEngine.dartExecutor.binaryMessenger, WifiApi(connectivityManager, flutterEngine.dartExecutor.binaryMessenger)) }この作業が完了したらandroid側のビルドが通るようになったはずです。
iOS側の導入設定
androidで作業が完了したら、対応する概念をiOS側にも実装する流れです。
android開発と同様にiOS開発ではXcodeを使った開発になるのかなと思われます。
また、Xcodeからビルドは通るが、Flutterコマンド経由でビルドできなかったときは
プロジェクト設定に齟齬がある場合があるので以下のコマンドでプロジェクト設定を確認すると良いかなと思われます。open ios/Runner.xcodeproject詳しくはFlutterでSwiftPackageManager利用時のパッケージ依存関係エラーを解消する方法に記載しているのでそちらもご覧ください。
1 Swift Package Managerでライブラリを入れる
ネットワークの接続情報でAndroidは標準ライブラリが使いやすかったので、何もいれませんでしたが、
iOSはちょっと触りづらいとのことで【Swift, iOS】iOS11以降のNetwork Reachabilityを参考にReachability.swiftを使いました。導入方法は
PodsやCarthageもありますがSwift Package Managerが一番扱いやすくておすすめです。
File>Swift Packages>Add Package Dependencyを選んで
https://github.com/ashleymills/Reachability.swiftを
Enter Package repository URLに入力して完了です
2 Xcodeで生成したファイルをリンク及びSwiftから呼び出し可能にする
iOSのビルドに取り込まいといけない生成ファイルが以下の2つです
* 'ios/Runner/Pigeon.h'(ヘッダーファイル)
* 'ios/Runner/Pigeon.m'(Object-Cファイル)Runnerディレクトリを右クリックして
Add Files to Runnerから生成したファイルをビルドに含めるようにします。また、実装そのものはSwiftで行うのでObject-Cで生成されたファイルを呼ぶために
Runner-Bridging-Header.hに以下の記述を追加します。#import "Pigeon.h"3. APIを実装する
基本的なコードの流れAndroidと同じはずです。
各名称にFlutterというPrefixをつける設定を付与しているので適宜読み変えていただけるとありがたいです。https://github.com/glassmonkey/flutter_wifi/blob/master/ios/Runner/WifiApi.swift
import Foundation import Reachability class Api: FlutterApi { init(_ flutterBinaryMessenger: FlutterBinaryMessenger) { self.callbackApi = FlutterCallbackApi(binaryMessenger: flutterBinaryMessenger) } var isWifi = false var isMobile = false let callbackApi: FlutterCallbackApi let reachability = try! Reachability() func call(_ input: FlutterWifiRequest, error: AutoreleasingUnsafeMutablePointer<FlutterError?>) -> FlutterWifiResponse? { let isDetect = input.isDetect as! Bool if isDetect { subscribe() } else { self.reachability.stopNotifier() } return self.fetchStatus(isDetect) } func subscribe() { reachability.whenReachable = { reachability in switch reachability.connection { case .wifi: self.isWifi = true self.isMobile = false case .cellular: self.isWifi = false self.isMobile = true default: self.isWifi = false self.isMobile = false } DispatchQueue.main.async { self.callbackApi.apply(self.fetchStatus(), completion: { (error: Error?) -> Void in }) } } try! self.reachability.startNotifier() } func fetchStatus(_ isDetect: Bool = true) -> FlutterWifiResponse { let response = FlutterWifiResponse() response.availableMobile = self.isMobile as NSNumber response.availableWifi = self.isWifi as NSNumber response.availableDetect = isDetect as NSNumber return response } }Androidと同じようにメインスレッドでで動作するようにしていますが、今回に限っていうと呼び出し元もメインスレッドなので不要だったりはします(今回はkotlinとの対称性の関係で明示的に書いておきました)
DispatchQueue.main.async { self.callbackApi.apply(self.fetchStatus(), completion: { (error: Error?) -> Void in }) }またコールバック初期化時にBinaryMessengerを渡すことを忘れないでください。
渡さなくてもビルドが通るのが厄介です。
忘れていると、コールバックの動作が無効化されるので、ボタンが押されたときのみ画面更新するような挙動になります。self.callbackApi = FlutterCallbackApi(binaryMessenger: flutterBinaryMessenger)4. APIを登録する
FlutterAppDelegate内の
func application内で以下の登録処理を行います。
- FlutterViewControllerから送受信用のbinaryMessengerを利用する
- FlutterApiSetupを実行する
https://github.com/glassmonkey/flutter_wifi/blob/master/ios/Runner/AppDelegate.swift
override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { let controller: FlutterViewController = window?.rootViewController as! FlutterViewController FlutterApiSetup(controller.binaryMessenger, Api(controller.binaryMessenger)) ... 略 ... }この作業が完了したらiOS側のビルドが通るようになったはず
おわりに
Pigeonに関してサンプルアプリを通した実装方法の解説をしました。
いかがだったでしょうか。私のような素人レベルでもPigeonのおかげでコンパイル前にエラーが得られるようになったので開発はかなりしやすかったです。
Pigeonを使うことで、ネイティブとFlutterで1つの定義ファイルを利用することになるので、
チーム開発で分担する場合でも認識揃えに良いと思いました。また、ネイティブのロジックはユニットテストを書くなどで堅実にすすめつつ、
テストが書きづらいビュー周りはFlutterのホットリロードを活用しすといった組み合わせができる点は
チーム開発では大きな武器になるなと思いました。サンプルアプリでは同期的な呼び出しと非同期の呼び出しに対応できるように
HostApiとFlutterApiの両方の実装を試してみましたが、FlutterApiな実装はサンプルコード含めてほとんどなかったので調査と実装には少し苦労しました。それでも生成されたコードがシンプルだったので読めば理解できました。細かい内容はまだバージョン1未満なので変わる可能性はありますが、
流れとか思想的なところはキャッチアップしてて損はないと思いました。最悪生成されたコードを読めばなんとかなる
現時点でも使えるレベルだとは思ったので、積極的に自分の開発では導入しようと思いました。








































