- 投稿日:2019-04-17T23:23:37+09:00
【Swift】UITableViewのセルのタップでキーボードを表示する
1.はじめに
テーブルビューのセルにUITextFieldを設置し、セルのタップでテキストフィールドのキーボードを表示させます。
その際、テキストフィールドをタップしたことによるキーボードの表示は避けます。
なぜかというと、テキストフィールドをタップしてキーボードを表示した場合、セルが選択状態にならないからです。
あくまでもtableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
からキーボードを表示させるのが目的です。セルの選択契機とテキストフィールドのタップ契機では微妙に見た目が違うので、セルの選択のみを契機としてキーボードを表示させます。
2.テキストフィールドの設定
isUserInteractionEnabled
をfalse
にする。これはstoryboardから設定できます。この設定により、テキストフィールドのタップを無効化します。
canBecomeFirstResponder
をtrue
にする。これによってテキストフィールドでキーボードを表示できるようします。ただ、このプロパティはget onlyのため、テキストフィールドを継承したサブクラスを定義し、そこで設定します。class TextFieldSub: UITextField { override var canBecomeFirstResponder: Bool { return true } }3.セルの選択時にキーボードを表示する
あとはUITableViewDelegate
のメソッドでセルのタップを捕まえてキーボードを表示するだけです。func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let cell = tableView.cellForRow(at: indexPath) // 選択したセルの取得 cell.textField.becomeFirstResponder() // セルのテキストフィールドでキーボードを表示する }
- 投稿日:2019-04-17T22:57:44+09:00
Deployment Targetを11.0以上にしたらArchive Buildできなくなったときの対処方法
対象になりそうなプロジェクト
- iOS 10以下のサポートを切るために
Deployment Target
を11.0にしようとしている- OSSをCarthageで利用している
Embedded Framework
をプロジェクト内で自作して利用している。- Build PhasesのRun Script
*1
にてlipoコマンドを利用しFrameworkのSlicing
をしている(-extractなど利用)通常のアプリのサポートOS version変更方法
- ProjectのDeployment targetを最低動作保証バージョンに設定する
iOS10.x以下からiOS11.xにする場合の変更方法
- 対応方法1
- ProjectのDeployment targetを最低動作保証バージョンに設定する
- アプリのTargetのDeployment targetを最低動作保証バージョンに設定する
- 自作Embedded frameworkのtargetは11.0未満にする(指定されていなければtargetに設定しfat fileにする)
- 対応方法2
- non fat fileに対してはSlicingを行わないscriptに変更
対応になぜ差があるのか
Deployment Targetの設定がbuildされるBinaryのarchitectureに影響する
Release build時のDeployment Targetが
11.0
以上の場合はarm64
のnon fat file
としてbuildされる。(10.x
以下だとarmv7
,arm64
のfat file
)
そのため、embedded framework
もarm64
のnon fat file
でbuildされ、*1
のscriptでerrorとなる。その他、Deployment targetを11以上に設定してビルドエラーのとき確認したいこと
- 32bitのArchitecture(
armv7
,armv7s
...)をProject
orTarget
->Architecture
とValid Architectures
に同時に設定していないか。(Architecture
が${ARCHS_STANDARD}
の場合確認不要)- Carthageでbuildされたbinaryを取得し、そのまま利用していないか。
- Embedded Frameworkのdeployment targetがarchitectureにbuild時の影響しないか
現時点で不要だった対応(2019/04/17)
- info.plistの
Required Device Capability
をarmv7
からarm64
への変更
- 投稿日:2019-04-17T22:57:32+09:00
CGImage.cropping()の注意点
はじめに
この前リリースしたAR Mini SketchというアプリでUIImageView上の画像をユーザーが指定した範囲で切り抜くという処理があるが、画像によっては指定通りに切り抜かれないという不具合があった。
原因を調べてみたところ、CGImage.cropping()の際の範囲指定には元画像の向きを考慮しなければいけないことが分かったため、注意点を備忘として残しておく。
CGImageクラスとは
CGImageクラスとはCore Graphics Imageの略。
bitmapイメージとして画像のマスク処理や切抜き処理を行うことができる。尚、このクラスにはwidthとheightというプロパティはあるがorientationというプロパティはない。
UIImageクラスとは
iOSアプリ上で画像を操作する際の最も一般的なクラス。
リソース上の画像ファイルを読み込む際も大抵このクラスのインスタンスとして操作することが多い。
UIImage.cgImageでこの画像のGCImageを参照することができる。尚、このクラスにはorientationというプロパティがあり画像の向き情報を保持している。
CGImage.cropping()とは
定義
func cropping(to rect: CGRect) -> CGImage?rectで指定した範囲を切り抜いて、新しいCGImageのインスタンスを返してくれる
注意点
rect は切り抜きたいCGImageのスケールで指定する必要がある。
具体的な例を示すと、400x200のサイズのCGImageの丁度真ん中で50x50で画像を切り抜きたい場合、CGRectのパラメータは次の様になる。let rect = CGRect(x: 200 - 25, y: 100 - 25, width: 50, height: 50)また、大抵UIImageViewと表示するUIImageのサイズは一致しないため、UIImageView上で指定された範囲は実際のCGImageのスケールに直す必要がある。
例えば上記の図のようにUIImageViewの表示上のサイズが400x1000で実際のUIImageのサイズが800x1200だったとした場合、選択範囲のRectは次のようにスケールを変更しなければならない。
let transform = CGAffineTransform(scaleX: 2.0, y: 1.2) rect.applying(transform)本題
じゃあ毎回、UIImageViewのサイズとUIImage(CGImage)のサイズを測って指定範囲のスケールを変更すれば良いのね、という訳ではなく一つ落とし穴がある。
ここのサイトの説明が分かり易いが画像にはorientationというプロパティが保持されており、UIImageView上で縦向きに画像が表示されていたとしてもCGImageの段階で横になっているということがある。
そうなると、下の図のように単純にスケールを変更しただけでは意図した場所から外れてしまうことになる。
したがって、UIImageView上で指定された範囲でCGImage.cropping()を行う際には次のロジックで処理を行う必要がある。
1. UIImageViewと表示しているUIImageの縦横のスケール比率を取得する
2. UIImageのorientationを取得し、画像の向きを判定する
3. 画像の向きに応じて範囲の回転並びに、平行移動を行う
4. 先ほど計算したスケール比率で範囲の拡大/縮小を行う
実際に書くとこんな感じ
extension UIImageView { func transformByImage(rect : CGRect) -> CGRect? { guard let image = self.image else { return nil } let imageSize = image.size let imageOrientation = image.imageOrientation let selfSize = self.frame.size let scaleWidth = imageSize.width / selfSize.width let scaleHeight = imageSize.height / selfSize.height var transform: CGAffineTransform switch imageOrientation { case .left: transform = CGAffineTransform(rotationAngle: .pi / 2).translatedBy(x: 0, y: -image.size.height) case .right: transform = CGAffineTransform(rotationAngle: -.pi / 2).translatedBy(x: -image.size.width, y: 0) case .down: transform = CGAffineTransform(rotationAngle: -.pi).translatedBy(x: -image.size.width, y: -image.size.height) default: transform = .identity } transform = transform.scaledBy(x: scaleWidth, y: scaleHeight) return rect.applying(transform) } }こんな感じで使います。
let cropRect = self.cropRect let imageView = self.imageView guard let image = imageView?.image else { return } guard let rectByImage = imageView?.transformByImage(rect : cropRect) else { return } let croppedImage = image.cgImage.cropping(to: rectByImage) let newImage = UIImage(cgImage: croppedImage!, scale: imageView.scale, orientation: image.imageOrientation)もし誰かのお役に立てば嬉しいです。
- 投稿日:2019-04-17T15:05:07+09:00
iOS標準のCLGeocoderを使って郵便番号から住所を取得する
郵便番号から住所を取得する
CLGeocoderには標準で
func geocodeAddressString(_ addressString: String, completionHandler: @escaping CLGeocodeCompletionHandler)こちらの郵便番号から住所を取得するメソッドがあるのですが、これで取得するとCLPlacemarkの
subLocality
が取得できませんでした。
そのため、一度このメソッドを使って取得した住所の緯度経度を使ってCLLocationを作りfunc reverseGeocodeLocation(_ location: CLLocation, completionHandler: @escaping CLGeocodeCompletionHandler)こちらのCLLocationから住所を取得するメソッドを使うことで
subLocality
が取得することができます。扱いやすいようにCLGeocoderにextensionで上記の方法で郵便番号から住所を取得するメソッドを追加すると良いかと思います。
extension CLGeocoder { struct Address { var administrativeArea: String? // 都道府県 例) 東京都 var locality: String? // 市区町村 例) 墨田区 var subLocality: String? // 地名 例) 押上 } func convertAddress(from postalCode: String, completion: @escaping (Address?, Error?) -> Void) { CLGeocoder().geocodeAddressString(postalCode) { (placemarks, error) in if let error = error { completion(nil, error) return } if let placemark = placemarks?.first { let location = CLLocation( latitude: (placemark.location?.coordinate.latitude)!, longitude: (placemark.location?.coordinate.longitude)! ) CLGeocoder().reverseGeocodeLocation(location) { placemarks, error in guard let placemark = placemarks?.first, error == nil else { completion(nil, error) return } var address: Address = Address() address.administrativeArea = placemark.administrativeArea address.locality = placemark.locality address.subLocality = placemark.subLocality completion(address, nil) } } } } }// 郵便番号はハイフンなしでも取得可能です CLGeocoder().convertAddress(from: "131-0045") { (address, error) in if let error = error { print(error) return } print(address?.administrativeArea) // → 東京都 print(address?.locality) // → 墨田区 print(address?.subLocality) // → 押上 }
- 投稿日:2019-04-17T14:00:14+09:00
MVCパターンについて理解する ~iOS開発~
MVC(Model-View-Controller)
レイヤー 役割 Controller ユーザーの入力を受け、Modelにコマンドを送る Model コマンドを受けて処理を行い、自信を更新 View Modelの変更を監視し、自信を更新 iOS開発でのMVCの役割
[Model]
- データ構造の表現
- Web APIとのやりとり
- ローカルデータベースなどへのデータ永続化
- データの振る舞いに関するロジック
View
- UIの表示
- Controllerからデータを受け取りUIに反映させる
- ユーザーインタラクションを認知
Controller
- Modelからデータを受け取りViewに渡してUIを更新する
- ライフサイクル処理や画面遷移などの処理を行う
サンプル
タスク管理アプリを例にしております。
Model層
Task.swift: データを表すオブジェクト
Task.swiftimport Foundation class Task { let text: String let deadline: Date init(text: String, deadline: Date) { self.text = text self.deadline = deadline } init(from dictionary: [String: Any]) { self.text = dictionary["text"] as! String self.deadline = dictionary["deadline"] as! Date } }TaskDataManager.swift: データに関するロジック
TaskDataManager.swiftimport Foundation class TaskDataManager: NSObject { private var tasks = [Task]() // UserDefaultsから保存したデータを取得 func loadData() { let userDefaults = UserDefaults.standard guard let taskDictionaries = userDefaults.object(forKey: "tasks") as? [[String: Any]] else { return } for dic in taskDictionaries { let task = Task(from: dic) self.tasks.append(task) } } func save(task: Task) { self.tasks.append(task) var taskDictionaries = [[String: Any]]() for t in self.tasks { let taskDictionary: [String : Any] = ["text": t.text, "deadline": t.deadline] taskDictionaries.append(taskDictionary) } let userDefaults = UserDefaults.standard userDefaults.set(taskDictionaries, forKey: "tasks") userDefaults.synchronize() } func count() -> Int { return self.tasks.count } func data(at index: Int) -> Task? { if self.tasks.count > index { return tasks[index] } return nil } }View層
UITableViewCellを継承したクラス
TaskCell.swiftimport UIKit class TaskCell: UITableViewCell { private var taskLabel: UILabel! private var deadlineLabel: UILabel! override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) self.taskLabel = UILabel() self.taskLabel.textColor = .black self.taskLabel.font = UIFont.systemFont(ofSize: 14) contentView.addSubview(self.taskLabel) self.deadlineLabel = UILabel() self.deadlineLabel.textColor = .black self.deadlineLabel.font = UIFont.systemFont(ofSize: 14) contentView.addSubview(self.deadlineLabel) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func layoutSubviews() { super.layoutSubviews() self.taskLabel.frame = CGRect(x: 15.0, y: 15.0, width: self.contentView.frame.width - 30, height: 15.0) self.deadlineLabel.frame = CGRect(x: self.taskLabel.frame.origin.x, y: self.taskLabel.frame.maxY + 8, width: self.taskLabel.frame.width, height: 15.0) } var task: Task? { didSet { guard let t = task else { return } self.taskLabel.text = t.text let formatter = DateFormatter() formatter.dateFormat = "yyyy/MM/dd" self.deadlineLabel.text = formatter.string(from: t.deadline) } } override func awakeFromNib() { super.awakeFromNib() } override func setSelected(_ selected: Bool, animated: Bool) { super.setSelected(selected, animated: animated) } }CreateTaskView.swiftimport UIKit // CreateTaskViewControllerへユーザーインタラクションを伝達するためのプロトコル protocol CreateTaskViewDelegate: class { func createView(taskEditting view: CreateTaskView, text: String) func createView(deadlineEditting view: CreateTaskView, deadline: Date) func createView(saveButtonDidTap view: CreateTaskView) } class CreateTaskView: UIView { private var taskTextField: UITextField! private var datePicker: UIDatePicker! private var deadlineTextField: UITextField! private var saveButton: UIButton! weak var delegate: CreateTaskViewDelegate? required override init(frame: CGRect) { super.init(frame: frame) self.taskTextField = UITextField() self.taskTextField.delegate = self self.taskTextField.tag = 0 self.taskTextField.placeholder = "予定を入れてください" self.addSubview(self.taskTextField) self.deadlineTextField = UITextField() self.deadlineTextField.tag = 1 self.deadlineTextField.placeholder = "期限を入れてください" self.addSubview(self.deadlineTextField) self.datePicker = UIDatePicker() self.datePicker.datePickerMode = .dateAndTime self.datePicker.addTarget(self, action: #selector(datePickerValueChanged(_:)), for: .valueChanged) // deadlineTextFieldが編集モードになった時に、キーボードではなくUIDatePickerになるようにする self.deadlineTextField.inputView = self.datePicker self.saveButton = UIButton() self.saveButton.setTitle("保存する", for: .normal) self.saveButton.setTitleColor(.black, for: .normal) self.saveButton.layer.borderWidth = 0.5 self.saveButton.layer.cornerRadius = 4.0 self.saveButton.addTarget(self, action: #selector(saveButtonTapped(_:)), for: .touchUpInside) self.addSubview(self.saveButton) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } @objc func datePickerValueChanged(_ sender: UIDatePicker) { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy/MM/dd HH:mm" let deadlineText = dateFormatter.string(from: sender.date) self.deadlineTextField.text = deadlineText self.delegate?.createView(deadlineEditting: self, deadline: sender.date) } @objc func saveButtonTapped(_ sender: UIButton) { self.delegate?.createView(saveButtonDidTap: self) } override func layoutSubviews() { super.layoutSubviews() self.taskTextField.frame = CGRect(x: bounds.origin.x + 30, y: bounds.origin.y + 30, width: bounds.size.width - 60, height: 50) self.deadlineTextField.frame = CGRect(x: self.taskTextField.frame.origin.x, y: self.taskTextField.frame.maxY + 30, width: self.taskTextField.frame.size.width, height: self.taskTextField.frame.size.height) let saveButtonSize = CGSize(width: 100, height: 50) self.saveButton.frame = CGRect(x: (bounds.size.width - saveButtonSize.width) / 2, y: self.deadlineTextField.frame.maxY + 20, width: saveButtonSize.width, height: saveButtonSize.height) } } extension CreateTaskView: UITextFieldDelegate { func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { delegate?.createView(taskEditting: self, text: textField.text ?? "") return true } }Controller層
ModelとViewの仲介役。
TaskDataManagerからデータを受け取りTaskCellに反映させるTaskListViewController.swiftimport UIKit class TaskListViewController: UIViewController { var dataSource: TaskDataManager! var tableView: UITableView! override func viewDidLoad() { super.viewDidLoad() self.dataSource = TaskDataManager() self.tableView = UITableView(frame: self.view.bounds, style: .plain) self.tableView.register(TaskCell.self, forCellReuseIdentifier: "Cell") self.tableView.delegate = self self.tableView.dataSource = self self.view.addSubview(self.tableView) let barButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(barButtonTapped(_:))) self.navigationItem.rightBarButtonItem = barButton } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) self.dataSource.loadData() self.tableView.reloadData() } @objc func barButtonTapped(_ sender: UIBarButtonItem) { // タスク作成画面へ遷移 let controller = CreateTaskViewController() let navi = UINavigationController(rootViewController: controller) self.present(navi, animated: true, completion: nil) } } extension TaskListViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return self.dataSource.count() } func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return 68.0 } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as? TaskCell else { return UITableViewCell() } let task = self.dataSource.data(at: indexPath.row) cell.task = task return cell } }タスク作成画面
CreateTaskViewController.swiftimport UIKit class CreateTaskViewController: UIViewController { fileprivate var createTaskView: CreateTaskView! fileprivate var dataSource: TaskDataManager! fileprivate var taskText: String? fileprivate var taskDeadline: Date? override func viewDidLoad() { super.viewDidLoad() self.view.backgroundColor = .white self.createTaskView = CreateTaskView() self.createTaskView.delegate = self self.view.addSubview(self.createTaskView) self.dataSource = TaskDataManager() } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() // createTaskViewのレイアウトを設定 self.createTaskView.frame = CGRect( x: self.view.safeAreaInsets.left, y: self.view.safeAreaInsets.top, width: self.view.frame.size.width - self.view.safeAreaInsets.left - self.view.safeAreaInsets.right, height: self.view.frame.size.height - self.view.safeAreaInsets.bottom ) } // 保存が成功した時のアラート fileprivate func showSaveAlert() { let alertController = UIAlertController(title: "保存しました", message: nil, preferredStyle: .alert) let action = UIAlertAction(title: "OK", style: .cancel) { action in self.navigationController?.popViewController(animated: true) } alertController.addAction(action) self.present(alertController, animated: true, completion: nil) } // タスクが未入力時のアラート fileprivate func showMissingTaskTextAlert() { let alertController = UIAlertController(title: "タスクを入力してください", message: nil, preferredStyle: .alert) let action = UIAlertAction(title: "OK", style: .cancel) { action in } alertController.addAction(action) self.present(alertController, animated: true, completion: nil) } // タスクが未入力時のアラート fileprivate func showMissingTaskDeadlineAlert() { let alertController = UIAlertController(title: "締切日を入力してください", message: nil, preferredStyle: .alert) let action = UIAlertAction(title: "OK", style: .cancel) { action in } alertController.addAction(action) self.present(alertController, animated: true, completion: nil) } } extension CreateTaskViewController: CreateTaskViewDelegate { // タスク内容を入力している時に呼ばれるデリゲートメソッド func createView(taskEditting view: CreateTaskView, text: String) { // CreateTaskViewからタスク内容を受け取りtaskTextに代入している self.taskText = text } // 締切日時を入力している時に呼ばれるデリゲートメソッド func createView(deadlineEditting view: CreateTaskView, deadline: Date) { self.taskDeadline = deadline } // 保存ボタンが押された時に呼ばれるデリゲートメソッド func createView(saveButtonDidTap view: CreateTaskView) { guard let taskText = self.taskText else { self.showMissingTaskTextAlert() return } guard let taskDeadline = self.taskDeadline else { self.showMissingTaskDeadlineAlert() return } let task = Task(text: taskText, deadline: taskDeadline) self.dataSource.save(task: task) self.showSaveAlert() } }