20190417のSwiftに関する記事は5件です。

【Swift】UITableViewのセルのタップでキーボードを表示する

1.はじめに
テーブルビューのセルにUITextFieldを設置し、セルのタップでテキストフィールドのキーボードを表示させます。
その際、テキストフィールドをタップしたことによるキーボードの表示は避けます。
なぜかというと、テキストフィールドをタップしてキーボードを表示した場合、セルが選択状態にならないからです。
あくまでもtableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) からキーボードを表示させるのが目的です。

セルの選択契機とテキストフィールドのタップ契機では微妙に見た目が違うので、セルの選択のみを契機としてキーボードを表示させます。

2.テキストフィールドの設定
isUserInteractionEnabledfalse にする。これはstoryboardから設定できます。この設定により、テキストフィールドのタップを無効化します。
canBecomeFirstRespondertrue にする。これによってテキストフィールドでキーボードを表示できるようします。ただ、このプロパティは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()           // セルのテキストフィールドでキーボードを表示する
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Deployment Targetを11.0以上にしたらArchive Buildできなくなったときの対処方法

対象になりそうなプロジェクト

  1. iOS 10以下のサポートを切るためにDeployment Targetを11.0にしようとしている
  2. OSSをCarthageで利用している
  3. Embedded Framework をプロジェクト内で自作して利用している。
  4. 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以上の場合はarm64non fat fileとしてbuildされる。(10.x以下だとarmv7,arm64fat file)
そのため、embedded frameworkarm64non fat fileでbuildされ、*1のscriptでerrorとなる。

その他、Deployment targetを11以上に設定してビルドエラーのとき確認したいこと

  1. 32bitのArchitecture(armv7,armv7s...)をProject or Target -> ArchitectureValid Architecturesに同時に設定していないか。(Architecture${ARCHS_STANDARD}の場合確認不要)
  2. Carthageでbuildされたbinaryを取得し、そのまま利用していないか。
  3. Embedded Frameworkのdeployment targetがarchitectureにbuild時の影響しないか

現時点で不要だった対応(2019/04/17)

  • info.plistのRequired Device Capabilityarmv7からarm64への変更
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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のスケールに直す必要がある。

スクリーンショット 2019-04-17 22.14.41.png

例えば上記の図のようにUIImageViewの表示上のサイズが400x1000で実際のUIImageのサイズが800x1200だったとした場合、選択範囲のRectは次のようにスケールを変更しなければならない。

let transform = CGAffineTransform(scaleX: 2.0, y: 1.2)
rect.applying(transform)

本題

じゃあ毎回、UIImageViewのサイズとUIImage(CGImage)のサイズを測って指定範囲のスケールを変更すれば良いのね、という訳ではなく一つ落とし穴がある。

ここのサイトの説明が分かり易いが画像にはorientationというプロパティが保持されており、UIImageView上で縦向きに画像が表示されていたとしてもCGImageの段階で横になっているということがある。

そうなると、下の図のように単純にスケールを変更しただけでは意図した場所から外れてしまうことになる。

スクリーンショット 2019-04-17 22.42.40.png

したがって、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)

もし誰かのお役に立てば嬉しいです。

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

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) // → 押上
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MVCパターンについて理解する ~iOS開発~

MVC(Model-View-Controller)

レイヤー 役割
Controller ユーザーの入力を受け、Modelにコマンドを送る
Model コマンドを受けて処理を行い、自信を更新
View Modelの変更を監視し、自信を更新
iOS開発でのMVCの役割

[Model]

  • データ構造の表現
  • Web APIとのやりとり
  • ローカルデータベースなどへのデータ永続化
  • データの振る舞いに関するロジック

View

  • UIの表示
  • Controllerからデータを受け取りUIに反映させる
  • ユーザーインタラクションを認知

Controller

  • Modelからデータを受け取りViewに渡してUIを更新する
  • ライフサイクル処理や画面遷移などの処理を行う

image.png

サンプル

タスク管理アプリを例にしております。

Model層

Task.swift: データを表すオブジェクト

Task.swift
import 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.swift
import 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.swift
import 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.swift
import 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.swift
import 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.swift
import 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()
    }

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