- 投稿日:2019-11-24T23:51:36+09:00
[Xcode]Wi-Fiでビルドする[実機]
開発中のiosアプリを実機(iPhone)でビルドする際に、USBケーブルを繋がずWi-Fiでビルドする方法です。
開発環境
macOS Catalina V.10.15.1
Xcode V.11.2.1やり方
1、最初はUSBケーブルで実機(iPhone)とMacを繋ぎます。
(このデバイスを信用しますか?と聞かれるので、「OK」)2、タブバーにある「Window」をクリックし、さらに「Devices and Simulators」をクリックします。
(ショートカットキーで⬆️+ ⌘ + 2 でもいい)3、すると、実機(iPhone)の情報が表示されます。そこにある「Connect via network」のチェックボックスにチェックを入れます。
これで完了です。試しにUSBを抜いてビルドしてみてください、正常にビルドできるはずです。
ただ、ビルドの速度はどうなんでしょう、、、やっぱり有線の方が速いかな?
- 投稿日:2019-11-24T22:20:08+09:00
【Swift/iOS】CoreLocationで緯度・経度を取得して位置情報を表示する
概要
CoreLocationで緯度・経度を取得して位置情報を表示する方法を説明します。
確認バージョン
Xcode 11.2.1
Swift 5.1.2
iOS 13.2.3CoreLocation
ライブラリ
TARGET -> Build Phase -> Link Binary With Libraries
CoreLocation.frameworkを追加
Info.plist
Privacy - Location When In Use Usage Description
位置情報許可のアラート画面に表示される説明を追加します。実装
- 情報表示用のUILabelを追加
- 位置情報を利用許可状態を判定
- 位置情報の更新開始
- CLLocationデータを逆ジオコーディングして位置情報を取得
- 位置情報が更新されるとUILabelに設定
import UIKit import CoreLocation class ViewController: UIViewController, CLLocationManagerDelegate { let locationManager = CLLocationManager() let geocoder = CLGeocoder() let text = [ "緯度", "経度", "国名", "郵便番号", "都道府県", "郡", "市区町村", "丁番なしの地名", "地名", "番地" ] var item: [ UILabel ] = [] var location: [ UILabel ] = [] override func viewDidLoad() { super.viewDidLoad() //サイズ let width = self.view.frame.width / 2 let height = self.view.frame.height / CGFloat( self.text.count + 1 ) //ラベル for ( i, text ) in text.enumerated() { //項目 self.item.append( UILabel() ) self.item.last!.frame.size = CGSize( width: width, height: height ) self.item.last!.frame.origin = CGPoint( x: 0, y: height * CGFloat( i + 1 ) ) self.item.last!.textAlignment = .center self.item.last!.text = text self.view.addSubview( self.item.last! ) //データ self.location.append( UILabel() ) self.location.last!.frame.size = CGSize( width: width, height: height ) self.location.last!.frame.origin = CGPoint( x: width, y: height * CGFloat( i + 1 ) ) self.location.last!.textAlignment = .center self.view.addSubview( self.location.last! ) } //ロケーションマネージャ self.locationManager.requestWhenInUseAuthorization() let status = CLLocationManager.authorizationStatus() if status == .authorizedWhenInUse { self.locationManager.delegate = self self.locationManager.distanceFilter = 10 self.locationManager.startUpdatingLocation() } } func locationManager( _ manager: CLLocationManager, didUpdateLocations locations: [ CLLocation ] ) { //表示更新 if let location = locations.first { //緯度・経度 self.location[0].text = location.coordinate.latitude.description self.location[1].text = location.coordinate.longitude.description //逆ジオコーディング self.geocoder.reverseGeocodeLocation( location, completionHandler: { ( placemarks, error ) in if let placemark = placemarks?.first { //位置情報 self.location[2].text = placemark.country self.location[3].text = placemark.postalCode self.location[4].text = placemark.administrativeArea self.location[5].text = placemark.subAdministrativeArea self.location[6].text = placemark.locality self.location[7].text = placemark.subLocality self.location[8].text = placemark.thoroughfare self.location[9].text = placemark.subThoroughfare } } ) } } }Githubのソースコードはこちら
※最新のコミットは下記の記事で説明するソースコードに更新しました。
【Swift/iOS】CoreLocationで取得した位置情報を住所に変換する実行
実行すると位置情報が表示されます。
シミュレータで実行する場合には場所を指定して下さい。
取得できる情報は言語設定にしたがってローカライズされています。
日本 JAPAN アメリカ合衆国 USA 参考
【CoreLocation】位置情報を取得する
[iOS] MapKitを使って”ジオコーディング・逆ジオコーディング”をやってみる開発アプリ
- 投稿日:2019-11-24T19:04:29+09:00
【Flutter】プラグインを使用した時に出るSwiftバージョン指定エラーの解決
問題
下記エラーの対処法です。
Flutterで特定のプラグインを使用した状態で、iOSでデバッグしようとすると発生します。
`app_review` does not specify a Swift version and none of the targets (`Runner`) integrating it have the `SWIFT_VERSION` attribute set. Please contact the author or set the `SWIFT_VERSION` attribute in at least one of the targets that integrate this pod.
app_review
の部分は使用するプラグインによって変わります。
SWIFT_VERSION
を指定しろ!!と言っています。もしかしたら、エラーメッセージの中に下の奴が黄色で強調されていて、エラーの本体のように見えてしまうかもしれませんが、上の奴が本体です。
WARNING: CocoaPods requires your terminal to be using UTF-8 encoding.注意
この記事の解決策を試す際には必ず
flutter cleanを実行してからビルドするようにしてください。
でないと、全然関係ないエラーを引き起こして混乱します。
解決策
ステップ1
まずはこちらの記事を、作者の方に感謝しながら参照して、XCode経由で、デフォルトのSwiftファイルとデフォルトのBridgingHeaderファイルを作成します。
https://qiita.com/ko2ic/items/f082f07df8a2aca6beed
これで解決すれば完了です。
ステップ2
それでも解決しない場合の解決策はこちらのissueのこのコメントです。
https://github.com/flutter/flutter/issues/16049#issuecomment-382629492
リンク先のソースコードをそのまま引用します(引用日:2019-11-24)
ios/Podfiletarget 'Runner' do use_frameworks! # <--- add this ... end post_install do |installer| installer.pods_project.targets.each do |target| target.build_configurations.each do |config| config.build_settings['ENABLE_BITCODE'] = 'NO' config.build_settings['SWIFT_VERSION'] = '3.2' # <--- add this end end end
ios/Podfile
の適切な場所に、use_frameworks!と
config.build_settings['SWIFT_VERSION'] = '3.2'の二行を追加してください。
これで解決すれば完了です。
ステップ3
ただ、おそらく、下記のようなエラーメッセージが出ると思います。
** BUILD FAILED ** Xcode's output: ↳ The “Swift Language Version” (SWIFT_VERSION) build setting must be set to a supported value for targets which use Swift. Supported values are: 4.0, 4.2, 5.0. This setting can be set in the build settings editor.この場合は、Swiftのバージョンをエラーメッセージで指定されている番号に書き換えてください。
config.build_settings['SWIFT_VERSION'] = '3.2'↓
config.build_settings['SWIFT_VERSION'] = '5.0'これで解決するはずです。少なくとも私は解決したんですよ?
- 投稿日:2019-11-24T18:49:06+09:00
FloatingPanelで高さを調整しようとして詰まった
この記事について
FloatingPanel というハーフモーダルビューの実装を数行でできてしまうツールを使おうとして、パネルの高さを調整しようとして詰まった 点のメモ。
Problem
FloatingPanelLayout
を以下のような形で実装してパネルの高さを調整しようとしたら、Panジェスチャーが指の方向とは真逆に動いたり、変な部分でカクついたりしてして挙動がおかしかった。
(以下は ドキュメント より抜粋)class ViewController: UIViewController, FloatingPanelControllerDelegate { ... func floatingPanel(_ vc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout? { return MyFloatingPanelLayout() } } class MyFloatingPanelLayout: FloatingPanelLayout { public var initialPosition: FloatingPanelPosition { return .tip } public func insetFor(position: FloatingPanelPosition) -> CGFloat? { switch position { case .full: return 16.0 // A top inset from safe area case .half: return 216.0 // A bottom inset from the safe area case .tip: return 44.0 // A bottom inset from the safe area default: return nil // Or `case .hidden: return nil` } } }Solution
floatingPanelController.delegate = self
をfloatingPanelController.set()
,floatingPanelController.addPanel
より後で読んでしまっていたのを、前
で呼ぶようにしたら直った。バグを吐いていた例
let contentVC = ContentViewController() fpc.set(contentViewController: contentVC) fpc.addPanel(toParent: self) fpc.delegate = self直った例
fpc.delegate = self let contentVC = ContentViewController() fpc.set(contentViewController: contentVC) fpc.addPanel(toParent: self)
- 投稿日:2019-11-24T16:43:30+09:00
【iOS】よくある横スクロールをStoryboardだけで作る
アプリのチュートリアル的な画面でよく見る、横にページめくりする感じのViewを実装します。
これをコードを全く書かずに作ります。サクッと作りたい時におすすめです。
それでは順を追って説明します。
UIScrollView、UIStackView(Horizontal)を配置
普通にスクロールビューを置いて制約(画面いっぱい)を設定し、中にスタックを置いています。
横スクロールにしたいのでスタックはHorizontalにし、スクロールビューとスタックビューの高さをあわせます。
(Stackをverticalに、横幅をあわせれば普通に縦スクロールになります。)UIScrollViewについて詰まったら(多分)いちばんシンプルなUIScrollViewの実装も見てみてください。
Stackの中にUIViewを配置
配置したViewに制約を設定
Viewの横幅を画面サイズ(UIScrollView)と同じにする制約をつけます。
(UIStackViewで高さは決まっているので横だけ決めればOKです。)
ここまでで Viewの個数 × 画面幅分 のスクロールができるようになります。UIScrollViewの「Paging Enabled」を有効に
このままだとただの横スクロールなので、
ページめくりのような動きにするために「Paging Enabled」を有効にします。
バウンス(端までスクロールした時に跳ねる感じのやつ)が不要であれば「Bounce On Scroll」も無効にしましょう。後は、先程いっぱい置いたViewに適宜部品を置いてチュートリアルを完成させましょう。
(今回は違いがわかるように背景色だけ変更しております。)以上で完成です。
余談
少しだけコードを記述すれば、UIPageControllerも追加できます。
こんな感じ。追加したコードは以下です。
レイアウトにはUIPageControlを追加し、ページ数だけ設定してあります。import UIKit class ViewController: UIViewController { @IBOutlet weak var scroll: UIScrollView! @IBOutlet weak var pageControl: UIPageControl! override func viewDidLoad() { super.viewDidLoad() } override func viewWillAppear(_ animated: Bool) { self.scroll.delegate = self } } extension ViewController:UIScrollViewDelegate { func scrollViewDidScroll(_ scrollView: UIScrollView) { let index = Int(round(scrollView.contentOffset.x / scrollView.frame.width)) self.pageControl.currentPage = index } }
不明点、不足点、改善点等ございましたらお教えいただけますと幸いです。
ありがとうございました。
- 投稿日:2019-11-24T16:39:30+09:00
[はじめてのiOSアプリ]xcodeで地図アプリを作成(その1)
はじめに
iOSアプリを作ってみたいけど
何から始めて良いのかわからないとりあえず、
「やってみました」記事を参考に
地図アプリを真似てみようと思うという記事の1回目です。
今回は、プロジェクトを作成します。
+αの目標
世の中に「やってみました」記事は、たくさんある。
多くの場合、同じことをすれば同じ結果を得ることができる。しかし「なぜそのようなことをするのか?」を
解決できないから自分が成長しない。『人が書いた「やってみました」記事を参考に
できるだけ「なぜ?」を解決しながらやってみよう』(汗)機能リスト
基本機能として、このような機能があればいいかな?
- 地図を表示
- GPS(corelocation)で現在位置取得し表示
- GPSと地図表示を連動
追加機能としては、これ。
- 縮尺を変更可能
- GPS 精度変更
- 逆 geocoding で地名表示
プロジェクト作成
- Xcode11 を起動
- 【なぜ?】
- iOS アプリを開発するには xcode を使う必要があるから
- 新規プロジェクトを作成
- [Create a new Xcode project]を選択
- 【なぜ?】
- 新しいアプリケーションを作成するから
- 新しいアプリケーションを作成するとき、最初に1回だけ行えばよい
- プロジェクト種別
- プロジェクト情報
- [Product Name]
- MyGpsMap
- 【なぜ?】
- 作ろうとしているアプリの名称として適切と思ったから
- 自分が作成するアプリ名称で重複しない文字列
- [Team]
- Personal Team
- 【なぜ?】
- None ではない、自分が所属している Team を選択
- 実は良くわかっていない
- [Organization Name]
- shinobee
- 【なぜ?】
- 自分が所属する組織を示す文字列
- 他人との重複は気にしなくて問題ない(と思う)
- 実は良くわかっていない
- [Organization Identifier]
- com.zen-zee.shinobee
- 【なぜ?】
- 他の人と重複しない文字列
- 今回は、自分が持っているドメイン+username を使用
- ドメインを持っていなければどうすれば良い?
- com.icloud.YourName で良いのか?
- 適当につけて、他人と重複したらどうなるのか?
- [Bundle Identifier]
- 変更できないので、そのまま
- [Language]
- Swift
- 【なぜ?】
- Swift を使いプログラムを書くから
- [User Interface]
- Storyboard を選択
- 【なぜ?】
- 従来からの UI 作成手段
- Swift UI のこと良く知らない、使ったことがない
- [Use Core Data]
- チェックを入れる
- 【なぜ?】
- iOS アプリの標準的なフレームワークなので入れておけば良い
- 実は良くわかっていない
- [Use CloudKit]
- チェックを入れない
- 【なぜ?】
- 今回は、iCloud を使わないから
- [Include Unit Tests]
- チェックを入れる
- 【なぜ?】
- 自動テスト関連
- 将来も使わなかもしれないけど、基本的に無害なので入れておけば良い
- [Include UI Tests]
- チェックを入れる
- 【なぜ?】
- 自動テスト関連?
- 将来も使わなかもしれないけど、基本的に無害なので入れておけば良い
- 実は良くわかっていない
- プロジェクト情報保存フォルダ
- ${HOME}/github/shinobee/
- 【なぜ?】
- この記事を見てください
- [Create Git repository on my Mac]にチェックを入れる
- 【なぜ?】
- ソースのバージョン管理に git を使用するから
- 将来も使わなかもしれないけど、基本的に無害なので入れておけば良い
- 作成したプロジェクト
今回の到達点
- これで良いのかわからないけど、とりあえず、xcodeのプロジェクトまで作成できた
連載
- 投稿日:2019-11-24T10:32:43+09:00
iOS端末の機能を使ったり、状態を管理したりしたいときに読む記事がこちら!
最近クロス開発プラットフォームの実用性を検証していたのですが、その中で、iOSネイティブならではの機能ってクロス開発でどう実装するんだろう?そもそもどんなのがあったっけ?とふと思うことがありましたので、iOSネイティブならではのiOS端末と密接に関わる機能の実装サンプルをまとめてみました。
開発環境はXcode11.0、Swift5.0で、最小構成で実装しています。
正直この記事を見た方が早いな...と思った項目に関してはリンクを掲載させていただいています。どの記事もわかりやくまとまっていて、とても勉強になるものばかりです。目次
- カメラ・アルバム
- 位置情報
- 住所
- 方角
- 端末イベント
- 監視
- 通知
- ネットワーク
- センサー
- スクリーンの明るさ
- 音
- 加速度
- 角加速度
- シェイク
- 歩数
- 認証
- 生体認証
- Sign In with Apple
- 端末システム情報
- モデル
- OS
- バッテリー残量
カメラ・アルバム
端末のカメラ・アルバムを使用する場合の実装です。ここでは写真・動画を撮影してアルバムに保存するまでを実装しています。
1.ユーザーに端末機能の使用許可を求めるため、Info.plistへ以下のKeyを追加
- Privacy - Camera Usage Description → カメラの使用
- Privacy - Photo Library Usage Description → アルバムの使用
- Privacy - Photo Library Additions Usage Description → アルバムへの保存
- Privacy - Microphone Usage Description → (動画を撮影する際の)マイクの使用
2.コーディング
class ImagePickerViewController: UIViewController { let imagePicker = UIImagePickerController() let photoTypes = ["public.image"] let movieTypes = ["public.movie"] /*省略*/ @objc func tapPhotoCameraButton() { pick(.photo) } @objc func tapMovieCameraButton() { pick(.video) } } extension ImagePickerViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate { // 写真・動画を撮影 func pick(_ mode: UIImagePickerController.CameraCaptureMode) { // 写真・動画のソースが使用可能かどうかをチェック guard UIImagePickerController.isSourceTypeAvailable(.camera) else { return } imagePicker.delegate = self // 写真・動画のソースの種類を設定 imagePicker.sourceType = .camera // 写真・動画の切り替えを設定 switch mode { case .photo: imagePicker.mediaTypes = photoTypes case .video: imagePicker.mediaTypes = movieTypes @unknown default: fatalError() } imagePicker.cameraCaptureMode = mode // 写真・動画の編集可否を設定 imagePicker.allowsEditing = true // 撮影画面を表示 present(imagePicker, animated: true) } // 撮影後の処理 func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { switch picker.mediaTypes { case photoTypes: // 写真を取得 guard let pickedImage = info[.originalImage] as? UIImage else { return } // アルバムに保存 UIImageWriteToSavedPhotosAlbum(pickedImage, nil, nil, nil) case movieTypes: // 動画を取得 guard let movieURL = info[.mediaURL] as? URL else { return } // アルバムに保存 UISaveVideoAtPathToSavedPhotosAlbum(movieURL.relativePath, nil, nil, nil) default: break } // 撮影画面を非表示 dismiss(animated: true) } }位置情報
端末の位置情報を使用する場合の実装です。ここでは住所と方角(端末の向き)を取得します。
1.ユーザーに位置情報の使用許可を求めるため、Info.plistへ以下のKeyを追加
- Privacy - Location When In Use Usage Description → アプリ使用時のみ使用
- Privacy - Location Always and When In Use Usage Description → 常に使用
2.コーディング
import CoreLocation import UIKit class LocationInfoReaderViewController: UIViewController { var locationManager: CLLocationManager? /*省略*/ } extension LocationInfoReaderViewController: CLLocationManagerDelegate { // 位置情報取得の許可状況に応じた処理 func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { switch status { case .authorizedAlways: return // 常に許可されている場合の処理 case .authorizedWhenInUse: locationManager?.requestAlwaysAuthorization() // アプリ使用時のみ許可されている場合の処理 case .restricted, .denied: return // 許可されていない場合の処理 case .notDetermined: locationManager?.requestWhenInUseAuthorization() // 許可するかどうかが設定されていない場合の処理 default: fatalError() } } enum UpdatingState { case locationStarted case locationStopped case headingStarted case headingStopped } // 位置情報の更新を開始・停止 func switchUpdatingState(to state: UpdatingState) { // 位置情報が使用可能かどうかをチェック guard CLLocationManager.locationServicesEnabled() else { return } switch state { case .locationStarted: locationManager?.startUpdatingLocation() // 緯度・経度の更新を開始 case .locationStopped: locationManager?.stopUpdatingLocation() // 緯度・経度の更新を停止 case .headingStarted: locationManager?.startUpdatingHeading() // 方角の更新を開始 case .headingStopped: locationManager?.stopUpdatingHeading() // 方角の更新を停止 } } // 位置情報の更新を開始した場合、更新される度に位置情報が下記のデリゲートメソッドに渡されます // 現在地の住所を取得 func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { // 緯度・経度を取得 guard let location = locations.last else { return } // 緯度・経度から場所の詳細情報を取得 CLGeocoder().reverseGeocodeLocation(location) { (placemarks, _) in guard let placemark = placemarks?.last else { return } self.addressInfoTextView.text = """ \(placemark.postalCode ?? "" /*郵便番号*/)\n \(placemark.administrativeArea ?? ""/*都道府県*/)\n \(placemark.locality ?? ""/*市町村*/)\n \(placemark.thoroughfare ?? ""/*地区*/)\n \(placemark.subThoroughfare ?? ""/*番地*/) """ } } // 方角(端末の向き)を取得 func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) { // 360°を45°ずつ8分割して方角を取得 let direction = { () -> String in switch (newHeading.magneticHeading / 360) * 8 { case 0.0 ..< 0.5, 7.5 ..< 8.0: return "北" case 0.5 ..< 1.5: return "北西" case 1.5 ..< 2.5: return "西" case 2.5 ..< 3.5: return "南西" case 3.5 ..< 4.5: return "南" case 4.5 ..< 5.5: return "南東" case 5.5 ..< 6.5: return "東" case 6.5 ..< 7.5: return "北東" default: fatalError() } } directionInfoLabel.text = direction() } }ちなみに特定の場所の住所を知りたい場合は、CLGeocoderクラスのgeocodeAddressStringメソッドの引数に地名(例:「東京タワー」)の文字列を渡してあげて緯度・経度を取得し、その緯度・経度を上の例と同様にreverseGeocodeLocationメソッドの引数に渡してあげると住所が取得できます。例えばUITextFieldと連携した場合は以下のようになりますね。
extension LocationInfoReaderViewController: UITextFieldDelegate { func textFieldShouldReturn(_ textField: UITextField) -> Bool { textField.resignFirstResponder() guard let place = textField.text else { return true } let geocoder = CLGeocoder() geocoder.geocodeAddressString(place) { (placemarks, _) in guard let location = placemarks?.last?.location else { return } geocoder.reverseGeocodeLocation(location) { (placemarks, _) in guard let placemark = placemarks?.last else { return } self.addressInfoTextView.text = """ \(placemark.postalCode ?? "")\n \(placemark.administrativeArea ?? "")\n \(placemark.locality ?? "")\n \(placemark.thoroughfare ?? "")\n \(placemark.subThoroughfare ?? "") """ } } return true } }端末イベント
端末で発生するイベントを監視・通知する場合の実装です。
監視
NotificationCenterクラスを使います。監視対象のイベントは色々とありますが、ここでは例としてアプリのライフサイクルと端末キーボードのイベントを監視しています。
class EventObserverViewController: UIViewController { /*省略*/ // 監視を開始 func setUpNotification() { [(#selector(appWillResignActive), UIApplication.willResignActiveNotification), (#selector(appDidEnterBackground), UIApplication.didEnterBackgroundNotification), (#selector(appWillEnterForeground), UIApplication.willEnterForegroundNotification), (#selector(appDidBecomeActive), UIApplication.didBecomeActiveNotification), (#selector(keyboardWillBecomeShown), UIApplication.keyboardWillShowNotification), (#selector(keyboardDidBecomeShown), UIApplication.keyboardDidShowNotification), (#selector(keyboardWillBecomeHidden), UIApplication.keyboardWillHideNotification), (#selector(keyboardDidBecomeHidden), UIApplication.keyboardDidHideNotification)] .forEach { (action, notificationName) in NotificationCenter.default.addObserver(self, selector: action, name: notificationName, object: nil) } } // 監視を停止(iOS9以降この処理は不要) func tearDownNotification() { NotificationCenter.default.removeObserver(self) } } // アプリライフサイクル関連のイベント発生時の処理 extension EventObserverViewController { @objc func appWillResignActive() { print("アプリがバックグランドに行きそうです") } @objc func appDidEnterBackground() { print("アプリがバックグランドに行きました") } @objc func appWillEnterForeground() { print("アプリがアクティブ状態になりそうです") } @objc func appDidBecomeActive() { print("アプリがアクティブ状態になりました") } } // キーボード関連のイベント発生時の処理 extension EventObserverViewController { @objc func keyboardWillBecomeShown() { print("キーボードが表示されそうです") } @objc func keyboardDidBecomeShown() { print("キーボードが表示されました") } @objc func keyboardWillBecomeHidden() { print("キーボードが閉じられそうです") } @objc func keyboardDidBecomeHidden() { print("キーボードが閉じられました") } }通知 - プッシュ通知
こちらをご参照ください。
- https://qiita.com/natsumo/items/ebba9664494ce64ca1b8
- https://qiita.com/k-yamada-github/items/258aec32a0d5b514f1cf
通知 - バッジ付与
標準フレームワークのUserNotificationsを使ってアプリのアイコンにバッジを付与します。
import UIKit import UserNotifications class UserNotifierViewController: UIViewController { var numberOfBadge = 0 { // この変数の更新後にバッジを付与 didSet { UIApplication.shared.applicationIconBadgeNumber = numberOfBadge } } /*省略*/ // 通知を許可するかどうかを確認 @objc func authorizeUserNotification() { let center = UNUserNotificationCenter.current() center.requestAuthorization(options: [.alert, .badge, .sound]) { (granted, error) in print("通知が\(granted ? "許可" : "拒否")された") } } // バッジ数を+1 @objc func plusBadge() { numberOfBadge += 1 } // バッジ数をリセット @objc func resetBadge() { numberOfBadge = 0 } }ネットワーク
ネットワークの接続状況を監視する場合の実装です。ネットワークや後で説明するセンサーの中には上で触れたNotificationCenterクラスの監視対象になるものもありますが、ネットワークやセンサーは大きな概念なので項目を分けています。
また、ネットワーク監視の実装方法として標準フレームワークにあるSCNetworkReachabilityクラスを直接使ったやり方もありますが、APIが非常にわかりくいので、ここではそのAPIをわかりやすくラッピングしたReachability.swiftというライブラリを使います。メンテなどが行き届いているのでけっこう使われているのではないでしょうか。
1.ライブラリのインストール → https://github.com/ashleymills/Reachability.swift
2.コーディング
import Reachability import UIKit class NetworkObserverViewController: UIViewController { let reachability = try! Reachability() /*省略*/ // ネットワークの監視を開始 func setUpNetworkObserver() { NotificationCenter.default.addObserver(self, selector: #selector(reachabilityChanged(note:)), name: .reachabilityChanged, object: reachability) do{ try reachability.startNotifier() }catch{ print("ネットワーク監視ができません") } } // ネットワークの監視を停止 func tearDownNetworkObserver() { reachability.stopNotifier() NotificationCenter.default.removeObserver(self, name: .reachabilityChanged, object: reachability) } // ネットワークの接続状況に応じた処理 @objc func reachabilityChanged(note: Notification) { guard let reachability = note.object as? Reachability else { return } switch reachability.connection { case .wifi: print("Wifiでつながりました") case .cellular: print("セルラーでつながりました") case .unavailable: print("ネットワークが切れました") case .none: print("ネットワーク監視ができません") } } }センサー
端末のセンサーを使用して以下のイベントを検知する場合の実装です。
スクリーンの明るさ
class BrightnessObserverViewController: UIViewController { /*省略*/ // スクリーンの明るさの監視を開始 func setUpNotification() { NotificationCenter.default.addObserver(self, selector: #selector(updateBrightnessInfo(_:)), name: UIScreen.brightnessDidChangeNotification, object: nil) } // スクリーンの明るさの監視を停止(iOS9以降この処理は不要) func tearDownNotification() { NotificationCenter.default.removeObserver(self, name: UIScreen.brightnessDidChangeNotification, object: nil) } // 明るさの表示を更新 @objc func updateBrightnessInfo(_ notification: Notification) { guard let screen = notification.object as? UIScreen else { return } brightnessInfoTextView.text = "\(String(format: "%.1f", screen.brightness * 100))%" } }音
こちらをご参照ください。
加速度
import CoreMotion import UIKit class AccelerationObserverViewController: UIViewController { let motionManager = CMMotionManager() /*省略*/ @objc func startObservingAcceleration() { // 加速度センサーが使用可能かどうかをチェック guard motionManager.isAccelerometerAvailable else { return } // 加速度の更新間隔(秒)を設定 motionManager.accelerometerUpdateInterval = 0.1 // 加速度の更新を開始 motionManager.startAccelerometerUpdates(to: OperationQueue.current!, withHandler: updateAcceleration()) } @objc func stopObservingAcceleration() { // 加速度センサーが使用可能かどうかをチェック guard motionManager.isAccelerometerActive else { return } // 加速度の更新を停止 motionManager.stopAccelerometerUpdates() } func updateAcceleration() -> CMAccelerometerHandler { return { (data, _) in // 加速度を更新中かどうかをチェック guard self.motionManager.isAccelerometerActive else { return } // 加速度(メートル毎秒毎秒)を取得 guard let acceleration = data?.acceleration else { return } // 加速度を小数第一まで取得 let formatted = { (acceleration: Double) -> String in return String(format: "%.1f", acceleration) } self.accelerationInfoTextView.text = """ x軸: \(formatted(acceleration.x)) y軸: \(formatted(acceleration.y)) z軸: \(formatted(acceleration.z)) """ } } }角加速度
import CoreMotion import UIKit class GyroObserverViewController: UIViewController { let motionManager = CMMotionManager() /*省略*/ @objc func startObservingGyro() { // 角加速度センサーが使用可能かどうかをチェック guard motionManager.isGyroAvailable else { return } // 角加速度の更新間隔(秒)を設定 motionManager.gyroUpdateInterval = 0.1 // 角加速度の更新を開始 motionManager.startGyroUpdates(to: OperationQueue.current!, withHandler: updateGyro()) } @objc func stopObservingGyro() { // 角加速度センサーが使用可能かどうかをチェック guard motionManager.isGyroActive else { return } // 角加速度の更新を停止 motionManager.stopGyroUpdates() } func updateGyro() -> CMGyroHandler { return { (data, _) in // 角加速度を更新中かどうかをチェック guard self.motionManager.isGyroActive else { return } // 角加速度(ラジアン毎秒毎秒)を取得 guard let gyro = data?.rotationRate else { return } // 角加速度を小数第一まで取得 let formatted = { (gyro: Double) -> String in return String(format: "%.1f", gyro) } self.gyroInfoTextView.text = """ x軸: \(formatted(gyro.x)) y軸: \(formatted(gyro.y)) z軸: \(formatted(gyro.z)) """ } } }シェイク
class ShakeObserverViewController: UIViewController { /*省略*/ override func viewDidLoad() { super.viewDidLoad() // イベントを最初に検知するオブジェクトに設定 becomeFirstResponder() } // イベントを最初に検知するオブジェクトになれるかどうかをチェック override var canBecomeFirstResponder: Bool { return true } // イベントを検知した際の処理 override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) { // イベントがシェイクかどうかをチェック guard motion == .motionShake else { return } shakeCount += 1 } }歩数
こちらをご参照ください。
認証
端末で認証を使う場合の実装です。ここでは生体認証とSign In with Appleについて触れています。
生体認証
ここでは顔・指紋認証を使います。(マイiPhoneがiPhoneSEなので顔認証は確認できてません)
1.ユーザーに顔認証機能の使用許可を求めるため、Info.plistへ以下のKeyを追加
- Privacy - Face ID Usage Description
2.コーディング
import LocalAuthentication import UIKit class BioAuthenticatorViewController: UIViewController { /*省略*/ @objc func executeBioAuth() { let authSystem = LAContext() var authError: NSError? // 認証機能が使用可能かどうかをチェック guard authSystem.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &authError) else { print("顔・指紋認証を使用できません"); return } // 認証方式に応じた処理(iOS11以降で使用可) let authMessage = { () -> String in guard #available(iOS 11.0, *) else { return "認証機能を使用します" } switch authSystem.biometryType { case .faceID: return "顔認証を使用します" case .touchID: return "指紋認証を使用します" default: fatalError() } } // 認証を実行 authSystem.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: authMessage()) { (granted, _) in print("認証に\(granted ? "成功" : "失敗")しました") } } }Sign In with Apple
こちらをご参照ください。
- https://qiita.com/shiz/items/5e094910f742c2ad72a4
- https://qiita.com/mejileben/items/df79024ae93971643271
端末システム情報
端末のシステム情報を使用する場合の実装です。ここでは端末のモデル名とOS、バッテリー残量を取得します。
class DeviceInfoReaderViewController: UIViewController { /*省略*/ func readDeviceInfo() -> String { // 使用中の端末を取得 let device = UIDevice.current // バッテリー監視を設定 device.isBatteryMonitoringEnabled = true return """ モデル: \(device.model) OS: \(device.systemName) \(device.systemVersion) バッテリー残量: \(device.batteryLevel * 100)% """ } }ふぅ...以上です!
- 投稿日:2019-11-24T09:05:00+09:00
【iOS】iOS13のUISearchBarの新機能を試してみる
iOS13では
UISearchBar
に関連する下記のクラスが追加されました。UISearchTextField
https://developer.apple.com/documentation/uikit/uisearchtextfieldUISearchToken
https://developer.apple.com/documentation/uikit/uisearchtokenUISearchTextFieldDelegate
https://developer.apple.com/documentation/uikit/uisearchtextfielddelegate色々調べてみたのですが
あまり情報が出てきませんでした。WWDC2019のセッションでもサンプルコードを出すと言っていたのですが
現在まだ見つけることができていません。
もし正式な情報の場所などご存知の方いらっしゃれば教えてください??♂️WWDC2019のセッション動画
https://developer.apple.com/videos/play/wwdc2019/224/?time=1276そこで
今回は実際にコードを動かしながら
どういう風に活用できるのかを検討した結果を記載したいと思います。各クラスの概要
UISearchTextField
こちらは元々非公開のプロパティでしたが
iOS13より公開され
UISearchBar
のテキストフィールドのカスタマイズが
より簡単にできるようになりました。下記のように
searchTextField
からアクセスできます。var searchBar = UISearchBar() searchBar.searchTextField.backgroundColor = .systemOrange searchBar.searchTextField.textColor = .systemPurple searchBar.searchTextField.font = UIFont(name: "American Typewriter", size: 18)このように設定すると下記の様にテキストフィールドの背景色とテキストの色を変更できます。
UISearchToken
こちらは検索キーワードを一個のタグのように
取り扱うことができます。例えば下記のように
searchTextField
プロパティへトークンを追加すると
下記の様に表示されます。let phoneToken = UISearchToken(icon: UIImage(systemName: "phone"), text: "電話") let messageToken = UISearchToken(icon: UIImage(systemName: "message"), text: "メッセージ") searchBar.searchTextField.insertToken(phoneToken, at: 0) searchBar.searchTextField.insertToken(messageToken, at: 0) searchBar.searchTextField.tokenBackgroundColor = .systemBlue※ トークンの色は
searchTextField
のプロパティに設定しているので
個々のトークンで色を決めることはできないようです。
(あったら嬉しいかなとちょっと思ったりしました。)UISearchTextFieldDelegate
searchTextField(_:itemProviderForCopying:)
というメソッドを一つ持つProtocolです。https://developer.apple.com/documentation/uikit/uisearchtextfielddelegate/3175446-searchtextfield
引数から見るとコピー&ペーストで使用するように思われます。
(WWDCでもコピー&ペーストと言っていました)こちら全然情報見つからなかったのですが
検した結果を後ほど記載させていただきたいと思います。実装
もう少し詳しく動作を確認するために
UISearchBar
を使って
UITableView
に表示されるデータの絞り込みをします。※ 動作の確認をするもので内容は適当ですので予めご了承ください。
事前準備
まず
下記のようなPlan
というenumを用意します。
3つのcaseがあって
それぞれにitemsが存在します。
UISearchBar
に入力されたキーワードの最初の2文字が一致したら
インスタンスを生成するようにinitを用意します。enum Plan: String, CaseIterable { case business case travel case shopping init?(_ text: String) { if text.starts(with: "bu") { self = .business } else if text.starts(with: "tr") { self = .travel } else if text.starts(with: "sh") { self = .shopping } else { return nil } } var items: [String] { switch self { case .business: return ["会議", "商談", "プレゼン", "勉強会"] case .travel: return ["宿泊", "日帰り"] case .shopping: return ["スーパー", "デパート"] } } var iconName: String { switch self { case .business: return "bag" case .travel: return "airplane" case .shopping: return "cart" } } }次に
UISearchBar
の設定です。class ViewController: UIViewController { @IBOutlet weak private var tableView: UITableView! private var searchController = UISearchController() private var searchBar: UISearchBar { searchController.searchBar } override func viewDidLoad() { super.viewDidLoad() setupSearchBar() } private func setupSearchBar() { searchController.searchResultsUpdater = self searchController.searchBar.placeholder = "検索したいキーワード" // UISearchTextField searchBar.searchTextField.backgroundColor = .systemOrange searchBar.searchTextField.textColor = .systemPurple searchBar.searchTextField.font = UIFont(name: "American Typewriter", size: 18) searchBar.searchTextField.tokenBackgroundColor = .systemBlue } }ここは上記で見たように表示方法などの設定を行っています。
全体は最後に記載させていただきますので
ここからは要点だけ見ていきます。トークンの作成
最初にトークンを作成する実装をしていきます。
下記のメソッドはUISearchBar
に入力された時に
トークンを作成するメソッドです。extension ViewController { private func setToken(from plan: Plan) { // 1 let planToken = UISearchToken(icon: UIImage(systemName: plan.iconName), text: plan.rawValue) // 2 planToken.representedObject = plan // 3 let field = searchBar.searchTextField field.replaceTextualPortion(of: field.textualRange, with: planToken, at: field.tokens.count) } }まず、1で
UISearchToken
のインスタンスを生成しています。2では
UISearchToken
が公開しているrepresentedObject
プロパティに
それぞれのトークンを識別するための値を設定できます。取得する際には下記のように
tokens
というプロパティから探します。private func extractSearchPlans() -> [Plan] { searchBar.searchTextField.tokens.compactMap { $0.representedObject as? Plan } }3で入力した文字列をトークンに置き換えています。
replaceTextualPortion
ですが
ソースコメントに下記のような説明が記載されています。Removes any text contained in the specified range, inserts the provided token at the specified index, and selects the newly-inserted token. Does not replace any tokens within the provided range. If the range intersects the marked text range, the marked text is committed. This method is essentially a convenience wrapper around the more fundamental `text`, `tokens`, and `selectedTextRange` properties, providing the selection behavior the user will expect. @note Because this method does not remove any tokens in the provided range, the caller can pass the field’s selectedTextRange to convert the selected portion of the text into a token without first having to trim the range.メソッドの引数で指定している
textualRange
には
下記のようなソースコメントが記載されています。The range that corresponds to the field’s text, exclusive of any tokens. @see -[<UITextInput> positionWithinRange:atCharacterOffset:]トークン部分を除いたテキストの範囲が設定されているようです。
動作
このような設定をすると
下記のような動きが実現できました。コピー&ペースト
ここからはコピー&ペーストの実装をしていきます。
まず
searchTextField
に下記の設定を追加します。searchBar.searchTextField.allowsCopyingTokens = trueどういう実装が必要なのかがわからなかったので
allowsCopyingTokens
のソースコードを見てみます。Whether the user can copy tokens to the pasteboard or drag them out of the text field. To support copying tokens, this property must be true and the delegate must provide an item provider for the tokens to be copied. UISearchTextField always enables the Copy command if any plain text is selected, even if the selection also includes tokens and this property is false. Defaults to true.ここからわかることとして
the user can copy tokens to the pasteboard
UIPasteboard
を使用するということと
this property must be true and the delegate must provide an item provider for the tokens to be copiedとあるのでdelegateの実装が必要になるようです。
UIPasteboard
の説明は割愛させていただきます。
↓をご参照ください。
https://developer.apple.com/documentation/uikit/uipasteboardUISearchTextFieldDelegateの実装
まず
UISearchTextFieldDelegate
の実装をしていきます。extension ViewController: UISearchTextFieldDelegate { func searchTextField(_ searchTextField: UISearchTextField, itemProviderForCopying token: UISearchToken) -> NSItemProvider { guard let plan = token.representedObject as? Plan else { return NSItemProvider() } return NSItemProvider(object: PastePlan(plan)) } }このメソッドはトークンのCopyをタップした際に呼ばれます。
ここでは
UIPasteboard
に設定するNSItemProvider
を用意しています。
NSItemProvider
の説明は割愛させていただきます。
↓をご参照ください。
https://developer.apple.com/documentation/foundation/nsitemprovider取得したい情報は
上記のsetTokenメソッドの中で設定した
representedObject
から取得しています。delegateの設定も忘れないように行います。
searchBar.searchTextField.delegate = selfUIPasteboardへ値の書き込みと読み込み
次に
UIPasteboard
へ値の書き込みと読み込みをするための実装をしていきます。そのために
UITextPasteDelegate
を実装します。
https://developer.apple.com/documentation/uikit/uitextpastedelegate使用するメソッドは
UITextPasteItem
が取得できるtextPasteConfigurationSupporting( _ textPasteConfigurationSupporting: UITextPasteConfigurationSupporting, transform item: UITextPasteItem)です。
extension ViewController: UITextPasteDelegate { func textPasteConfigurationSupporting(_ textPasteConfigurationSupporting: UITextPasteConfigurationSupporting, transform item: UITextPasteItem) { guard let item = item as? UISearchTextFieldPasteItem else { return } item.itemProvider.loadObject(ofClass: PastePlan.self) { (pastePlan, error) in guard let plan = (pastePlan as? PastePlan)?.plan else { return } let token = UISearchToken(icon: UIImage(systemName: plan.iconName), text: plan.rawValue) item.setSearchTokenResult(token) } } }
UISearchTextFieldPasteItem
はProtocolで
setSearchTokenResult(_ token: UISearchToken)
メソッドがあり
ここでトークンをUIPasteboard
に設定することができます。
UISearchTextFieldPasteItem
https://developer.apple.com/documentation/uikit/uisearchtextfieldpasteitem上記のコードで出てきている
PastePlan
は
NSItemProvider
に値を設定するために用意したクラスです。
(コードは最後に記載しています。)
内部でPlan
を保持するようにしています。またJSONとして値を取得するために
Codable
にも適合するようにしています。delegateは
searchTextField
が適合する
UITextPasteConfigurationSupporting
の
pasteDelegate
への設定が必要になります。searchBar.searchTextField.pasteDelegate = selfUIPasteboardからの値の取得
最後に
UIPasteboard
から値を取得します。private func extractPlanFromPasteboard() -> Plan? { guard let data = UIPasteboard.general.data(forPasteboardType: (kUTTypeJSON as String)) else { return nil } UIPasteboard.general.items = [] return try? JSONDecoder().decode(PastePlan.self, from: data).plan }※
最終的なトークンの設定や表示するデータのフィルターは
UISearchResultsUpdating
の
updateSearchResults(for searchController: UISearchController)
の中で行っています。動作
このような実装をすることで
下記のような動作が実現できました。今回使用したコード
下記に記載もしていますが
gistにもアップロードしました。https://gist.github.com/stzn/4e49e0afe3d4208a824e8eed53f24c61
全体のコード
import UIKit import MobileCoreServices class PastePlan: NSObject, NSItemProviderReading, NSItemProviderWriting, Codable { let plan: Plan init(_ plan: Plan) { self.plan = plan } static var readableTypeIdentifiersForItemProvider: [String] = [(kUTTypeJSON) as String] static var writableTypeIdentifiersForItemProvider: [String] = [(kUTTypeJSON) as String] static func object(withItemProviderData data: Data, typeIdentifier: String) throws -> Self { try JSONDecoder().decode(self, from: data) } func loadData(withTypeIdentifier typeIdentifier: String, forItemProviderCompletionHandler completionHandler: @escaping (Data?, Error?) -> Void) -> Progress? { let progress = Progress(totalUnitCount: 100) do { let data = try JSONEncoder().encode(self) progress.completedUnitCount = 100 completionHandler(data, nil) } catch { completionHandler(nil, error) } return progress } } enum Plan: String, CaseIterable, Equatable, Codable { case business case travel case shopping init?(_ text: String) { if text.starts(with: "bu") { self = .business } else if text.starts(with: "tr") { self = .travel } else if text.starts(with: "sh") { self = .shopping } else { return nil } } var iconName: String { switch self { case .business: return "bag" case .travel: return "airplane" case .shopping: return "cart" } } var items: [String] { switch self { case .business: return ["会議", "商談", "プレゼン", "勉強会"] case .travel: return ["宿泊", "日帰り"] case .shopping: return ["スーパー", "デパート"] } } } class ViewController: UIViewController { @IBOutlet weak private var tableView: UITableView! private var searchController = UISearchController() private var filteredItems: [[String]] = [] private var searchBar: UISearchBar { searchController.searchBar } private var isSearchBarEmpty: Bool { if !searchBar.searchTextField.tokens.isEmpty { return false } return searchBar.text?.isEmpty ?? true } override func viewDidLoad() { super.viewDidLoad() setupSearchBar() setupTableView() } private func setupSearchBar() { searchController.searchResultsUpdater = self searchController.obscuresBackgroundDuringPresentation = false searchBar.placeholder = "検索キーワード" definesPresentationContext = true // UISearchTextField searchBar.searchTextField.backgroundColor = .systemOrange searchBar.searchTextField.textColor = .systemPurple searchBar.searchTextField.font = UIFont(name: "American Typewriter", size: 18) searchBar.searchTextField.tokenBackgroundColor = .systemBlue // Delete searchBar.searchTextField.allowsDeletingTokens = true // Copy & Paste searchBar.searchTextField.allowsCopyingTokens = true searchBar.searchTextField.delegate = self searchBar.searchTextField.pasteDelegate = self } private func setupTableView() { tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") tableView.dataSource = self tableView.tableHeaderView = searchController.searchBar } } extension ViewController: UISearchTextFieldDelegate { func searchTextField(_ searchTextField: UISearchTextField, itemProviderForCopying token: UISearchToken) -> NSItemProvider { guard let plan = token.representedObject as? Plan else { return NSItemProvider() } return NSItemProvider(object: PastePlan(plan)) } } extension ViewController: UITextPasteDelegate { func textPasteConfigurationSupporting(_ textPasteConfigurationSupporting: UITextPasteConfigurationSupporting, transform item: UITextPasteItem) { guard let item = item as? UISearchTextFieldPasteItem else { return } item.itemProvider.loadObject(ofClass: PastePlan.self) { (pastePlan, error) in guard let plan = (pastePlan as? PastePlan)?.plan else { return } let token = UISearchToken(icon: UIImage(systemName: plan.iconName), text: plan.rawValue) item.setSearchTokenResult(token) } } } extension ViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { if isSearchBarEmpty { return Plan.allCases[section].items.count } if !filteredItems.isEmpty { return filteredItems[section].count } return 0 } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) var item: String? = nil if isSearchBarEmpty { item = Plan.allCases[indexPath.section].items[indexPath.row] } else if !filteredItems.isEmpty { item = filteredItems[indexPath.section][indexPath.row] } cell.textLabel?.text = item return cell } } extension ViewController: UITableViewDelegate { func numberOfSections(in tableView: UITableView) -> Int { if isSearchBarEmpty { return Plan.allCases.count } if !filteredItems.isEmpty { return filteredItems.count } return 0 } func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { Plan.allCases[section].rawValue } } extension ViewController: UISearchResultsUpdating { func updateSearchResults(for searchController: UISearchController) { var text = searchBar.text!.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) guard !isSearchBarEmpty else { return } var searchPlans: [Plan] = extractSearchPlans() if let plan = Plan(text) { setToken(from: plan) searchPlans.append(plan) text = "" } else if let plan = extractPlanFromPasteboard() { searchPlans.append(plan) text = "" } updateUI(plans: searchPlans, text: text) } private func extractPlanFromPasteboard() -> Plan? { guard let data = UIPasteboard.general.data(forPasteboardType: (kUTTypeJSON as String)) else { return nil } UIPasteboard.general.items = [] return try? JSONDecoder().decode(PastePlan.self, from: data).plan } private func extractSearchPlans() -> [Plan] { searchBar.searchTextField.tokens.compactMap { $0.representedObject as? Plan } } private func setToken(from plan: Plan) { let planToken = UISearchToken(icon: UIImage(systemName: plan.iconName), text: plan.rawValue) planToken.representedObject = plan let field = searchBar.searchTextField field.replaceTextualPortion(of: field.textualRange, with: planToken, at: field.tokens.count) } private func updateUI(plans: [Plan], text: String) { filteredItems = [] if !plans.isEmpty { filteredItems = Plan.allCases.filter { plans.contains($0) } .map { $0.items.filter { $0.starts(with: text) } } } else { filteredItems = Plan.allCases.map { $0.items.filter { $0.starts(with: text) } } } tableView.reloadData() } }まとめ
iOS13の
UISearchBar
の新機能について見ていきました。特に
UISearchToken
に関しては
検索キーワードを一つのまとまりとして扱ったりすることができ
使い道がたくさんあるのではないかと思います。ただ現在情報が見つからないため
正式なサンプルやドキュメントなどが
早く出てくると嬉しいですね?もし間違いなどございましたら教えていただけますとうれしいです??♂️
- 投稿日:2019-11-24T00:49:12+09:00
【Swift】OpenAPIGeneratorにPRを出してみる
いくつかのiOSアプリ案件でOpenAPIGeneratorを使ってswift4のAPIクライアントを自動生成しているのですが、とても快適です。
利用状況はこんな感じ
- OpenAPIClientで生成したファイルは、〇〇(プロジェクト名)APIClientとしてプロジェクト本体のレポジトリとは別のレポジトリとして管理
- SwiftPackageManagerに対応させて、SPM経由でプロジェクトからimportできるように
update.sh
というシェルを○○APIClientディレクトリのルートにおいて、実行してタグ更新するだけでAPIの更新が出来るようにし、人依存を撤廃- いくつか都合が悪いところは、static var の部分を修正して、カスタマイズして対応。(BuilderFactoryを丸ごと切り替えられるようになっているので、最終的に全部カスタマイズ可能な作りになっていてとても便利です)
別レポジトリに切り出したのは、APIClientはプロジェクトの内部構造に依存せず独立運用可能なので、切り離して運用する事でプロジェクトレポジトリの複雑度をむやみに上げないためというのが1つと、swift4のgeneratorがAlamofireの古いバージョンを利用しているのですが、プロジェクトディレクトリからどのバージョンのライブラリを使っているとかを一切意識せずに使えるようにしたかったからです。余計な依存性を含めたくありませんでした。
ただ、使っていて2点だけ困るところがあったので修正リクエストを送ってみようと思います。内容は以下です。
- レスポンスを受け取るQueueが選択できず、常にメインスレッドが使われるので切り替えられるようにしたい。
- RequestBuilderのときはNonDecodableBuilderになって欲しい。
ただ、ちょっと敷居が高くて手を出しにくいOSSに感じるので再現できるように自分の体験した手順を公開しながら進めて行こうと思います。
今回は1.のスレッドの件を題材にします。
それでは進めていきます。
1. Fork
プロジェクトをforkします。
2. Swift4のMustacheを管理しているディレクトリを発見する
まず、これがなかなか見つかりませんでした。
「GitHub OpenAPIGenerator Mustache」とグーグル検索して、色々ディレクトリを掘って探しているとmodules/openapi-generator/src/main/resourcesの下に見つけられました
3. レスポンスを受け取るQueueを選択できるようにしてみる
コールバックをmainキューに指定できるのはネットワークのライブラリではよくある事です。ただ、利用側が任意に切り替えられないのが不便だと思った点でした。そこでmasterブランチからresponse-queueというブランチを切って、提案を出してみようと思います。
カスタマイズ可能な仕組みの
open class {{projectName}}API
に対して、public static var queue: DispatchQueue = .mainという記述を加え、通信を行う
AlamofireImplementations.mustache
の中で、Alamofireに対してqueueを渡して通信するようにしてみます。いくつか変更を加えてみました。
https://github.com/syatyo/openapi-generator/commit/41428e08a6763c76df296d12c1235fe20d5ccfa1
https://github.com/syatyo/openapi-generator/commit/dac4f48c480dabcc07a2d1945feb31b24997f859
4. 自動生成を試してみる
みた感じうまくいってそう。
java -jar modules/openapi-generator-cli/target/openapi-generator-cli.jar generate -i https://raw.githubusercontent.com/openapitools/openapi-generator/master/modules/openapi-generator/src/test/resources/2_0/petstore.yaml -g swift4 -o ~/Desktop/TestAPIClient5. PRを出そうとする...前の準備
PRを出す前に、プロジェクト規約がしっかり決まっているようなので、こちらのファイルを良く読んでみます。
https://github.com/openapitools/openapi-generator/blob/master/CONTRIBUTING.mdBefore Submitting PRを読むと、「PR出す前にissue探してなかった場合はまずissue立ててくれ」と書いてあるので、issueを立ててみました。
割と支離滅裂な英語かもしれませんが、とりあえず書きたい事は書きました。
https://github.com/OpenAPITools/openapi-generator/issues/4590次にテストの章を実行していきます。
PetStoreのサンプルをアップデート
こちらを実行すれば良いっぽいです。
./bin/swift4-all.sh
テスト実行
テスト実行用の便利なshellを発見したので、これを使っていきます。
samples/client/test/swift4/swift4_test_all.sh
自分の場合Xcode11を使っているからかテストかうまく終わらず、理由もエラーを読んでも特定しにくいものだったので、
samples/client/petstore/swift4/default/SwaggerClientTests/SwaggerClient.xcworkspace
を開いて、Xcodeから直接テストを実行しました。特にテストケースの追加はしていないのでこのままコミットを行います。
6. 準備ができたのでいざPR
一通りガイドラインの準備事項が終わったので、PRを出していきます。
PRを出す時のチェックリストがあるのでいくつか雑に翻訳してながら見ていきます。
- ガイドライン読んで従ってね
- プロジェクトのビルドしてね
- サンプルの再出力してね
- 正しいブランチにPR向けてね
- コミッターの一覧に入ってる言語ごとのレビュワーを全部コピーしてメンションしてね
ざっくりこんな感じのはず。
全部確認したのでPRを出します。
完了!
https://github.com/OpenAPITools/openapi-generator/pull/4591
無事、PRを出すところまでは成功しました。ガイドラインが厳しめで結構大変でしたが、ほっと一息。
なんかビルドエラー出てるけど明日考えよう。何回か修正するとしても、マージまでいけると嬉しいなー。
それではお付き合いありがとうございました!みなさんがPR出す時のヘルプになれば良いと思います!
- 投稿日:2019-11-24T00:42:19+09:00