20210125のSwiftに関する記事は6件です。

エラー「Application tried to present modally a view controller」の時

環境

・Xcode: 12.3
・Swift5

以下のようなエラーログがでてクラッシュしてしまった時

libc++abi.dylib: terminating with uncaught exception of type NSException

*** Terminating app due to uncaught exception 'NSInvalidArgumentException',
 reason: 'Application tried to present modally a view controller <ProjectName.ViewController: 0x158331780> 
.that has a parent view controller <UINavigationController: 0x159877a00>.'
terminating with uncaught exception of type NSException

こんなパターンがあるよ

スクリーンショット 2021-01-25 23.26.29.png

上記のようにViewControllerに対してNavigationControllerがいる場合に、
ViewControllerを表示しようとしてしまっている可能性

        let storyboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
        if let navigationVc = storyboard.instantiateInitialViewController() as? UINavigationController,
           let viewController = navigationVc.topViewController as? ViewController {
            // これは×
            // present(viewController, animated: true, completion: nil)
            // これは○
            present(navigationVc, animated: true, completion: nil)
        }

これに限らないかと思いますが、誰かの役に立てば。

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

inputViewを使ってカスタムキーボードをつくろう!(Swift)

Xcode-12 Swift-5.3 iOS-14

はじめに

UITextFieldUITextView で入力する際はキーボードの入力タイプが色々用意されていますが実はこのキーボード色々カスタムができます!

たぶん実用的なのは UIPickerView とか UIDatePicker を表示したりとかです(なんか昔半角カナ用キーボードを作った記憶があるけどあんまよくなかった:innocent:)。

これからはもう UIKit を使うことも少なくなっていくのかもしれませんが備忘録として。。。

*注:「つくろう」って書きましたがとくにカスタムキーボードを薦めるものではありません。

UITextFieldをカスタマイズ

試しに UITextField のキーボードをカスタムしてみます。やり方は簡単で inputView に任意の View を設定するだけで OK です:v:

// こんなのとか
let keyboard = CustomKeyboardView(frame: .init(origin: .zero, size: .init(width: 284, height: 284)))
keyboard.delegate = self
textField.inputView = keyboard

// こんなのとか
let picker = UIPickerView()
picker.delegate = self
picker.dataSource = self
textField.inputView = picker

あとはデリゲートとかで入力文字を受け取って UITextField に表示するだけです。

こんな感じ。

カスタム View ピッカー
custom picker

カスタム View はこんな感じです。

xib

xib

protocol CustomKeyboardViewDelegate: AnyObject {
    func customKeyboardView(_ customKeyboardView: CustomKeyboardView, didSelectKey key: String)
}

final class CustomKeyboardView: UIView {

    weak var delegate: CustomKeyboardViewDelegate?
    override init(frame: CGRect) {
        super.init(frame: frame)
        loadNib()
    }

    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)!
        loadNib()
    }

    private func loadNib() {
        let view = Bundle.main.loadNibNamed("CustomKeyboardView", owner: self, options: nil)?.first as! UIView
        view.frame = bounds
        view.translatesAutoresizingMaskIntoConstraints = true
        view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        addSubview(view)
    }

    @IBAction private func selectKey(_ sender: UIButton) {
        delegate?.customKeyboardView(self, didSelectKey: sender.titleLabel!.text!)
    }
}

キーボードの高さはわりと自由みたいです:raised_hands:

custom_2

サクッとカスタムキーボードが表示できました!ただカスタムキーボードを入力制限目的で利用するにはいくつか問題があるのでオススメしません:no_good:コピペとか外部キーボード接続とか。。。

コピペに関しては UITextField のカスタムクラスをつくって canPerformAction ごにょごにょすればいける気がしますが外部キーボードに関しては防げないと思われます(そもそもあんまり UITextField で入力制限なんてしない方がいいと思います)。

UIControlをカスタマイズ

とくに UITextFieldinputView に設定する!でも問題ないのですが UITextField を使うとなるとレイアウトの自由度が低いのでせっかくなんでカスタムクラスをつくった方が自由度が高いのでおすすめです:thumbsup:

下記のように UIControl を継承したクラスをつくって canBecomeFirstResponderinputView を設定してやるとカスタムキーボードが表示できます。UIView じゃないのは addAction がしたかったからです。フォーカス時がわかりにくいので枠線の色を変更しています。

protocol PickerInputControlDelegate: AnyObject {
    func pickerInputControl(_ pickerInputControl: PickerInputControl, didSelectValue value: String)
}

final class PickerInputControl: UIControl {

    var items = [String]()
    weak var delegate: PickerInputControlDelegate?

    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }

    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)!
        commonInit()
    }

    private func commonInit() {
        layer.cornerRadius = 8.0
        layer.borderWidth = 1.0
        layer.borderColor = UIColor.systemGray.cgColor
        addAction(.init(handler: { [weak self] _ in
            self?.isSelected.toggle()
            if self?.isSelected == true {
                self?.becomeFirstResponder()
            } else {
                self?.resignFirstResponder()
            }
        }), for: .touchUpInside)
    }

    override var inputView: UIView? {
        let picker = UIPickerView()
        picker.delegate = self
        picker.dataSource = self
        return picker
    }

    override var canBecomeFirstResponder: Bool {
        return true
    }

    override var isSelected: Bool {
        didSet {
            layer.borderColor = isSelected ? UIColor.systemBlue.cgColor : UIColor.systemGray.cgColor
        }
    }

    @discardableResult
    override func becomeFirstResponder() -> Bool {
        let value = super.becomeFirstResponder()
        isSelected = isFirstResponder
        return value
    }

    @discardableResult
    override func resignFirstResponder() -> Bool {
        let value = super.resignFirstResponder()
        isSelected = isFirstResponder
        return value
    }
}

extension PickerInputControl: UIPickerViewDelegate {

    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        delegate?.pickerInputControl(self, didSelectValue: items[row])
    }
}

extension PickerInputControl: UIPickerViewDataSource {

    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 1
    }

    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        return items.count
    }

    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        return items[row]
    }
}

こんな感じで UIControl なので中に View 置き放題です。

control

UIPickerViewDelegate, UIPickerViewDataSource, items は外に出した方が汎用性が高いかも:innocent:

static cellと組み合わせる

おまけで UITableViewController の static cell と組み合わせるとフォーカス時に自動スクロールしてくれるので複数の入力項目がある場合はおすすめです:thumbsup:

こんな感じ。

static_cell

おわりに

カスタムキーボードを作る際はタップ領域をちゃんと確保してあげると素敵です。ちょっと古い記事ですが下記の記事はキーボードのタップ領域とか詳しく調べていておもしろいです。

iPhone の当たり判定を検証した

わりとお世話になった手法だけどこれからはあんまり使わなくなるのかな UIKit。。。

調べてて気になったけどこいつ知らねえ:sweat:

inputviewcontroller

参考

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

【Swift】RxSwiftが難しいのでサンプルコードを作った

RxSwiftが難しい

「RxSwiftについていろんな記事を読みまくったけど、結局わからん」
その気持ちめっちゃわかります。
難しいですよね〜特に独学だとめちゃめちゃ辛い。
とりあえずRxSwiftはデータバインディングができて、
それを利用してMVVMなんかに使われることが多いです。

RxCocoaと併用しよう

RxSwiftはRxCocoaとよく併用されます。

ViewModel

スクリーンショット 2021-01-25 16.45.27.png

ModelTypeでInputとOutputのプロトコルを返すことで、
viewModel.outputs.output、
viewModel.inputs.input
のようにそれがoutputsに登録したものなのか
inputsに登録したものなのかが明確になる。
extensionで準拠させよう。

inputsはViewControllerから受け取るイベントで、
受け取りなのでObserverを使う。

outputsはinputsでViewControllerから受け取ったイベントによって
処理をして、その結果をViewControllerに返すのでObservableを使う。

Observerは受信者(監視)、Observableは発信者であるので
それぞれoutputsとinputsで使い分けている。

output

let _output = BehaviorRelay<Bool>(value: false)
self.output = _output.asObservable()
//Observableを代入し変数outputを初期化している。

input

self.input = AnyObserver<Bool>() { bool in
//Observerなので、ViewControllerからinputに流れてきた
//イベントをここで受け取る。 boolはそのイベント。
    _output.accept(bool)
    //Observable(発信者)にイベントを発信させるには
    //accept()を使う。
}

こんな感じで、ViewModelはVCからイベントをinputで受け取り、
outputで処理結果を返す感じに使う。
VCではinputでViewModelにイベントを送り、
outputを監視して処理結果に応じたUIの更新を行う。

ViewController

スクリーンショット 2021-01-25 16.32.26.png
Instantiateを使うとbundleNameとか使わなくても、
storyboardを呼び出せる。(別々のstoryboardを使っていてもこれで一発なので超便利)
dependencyやinjectは依存性的な感じ。ここは自分で調べてください。

var viewModel: RxSwiftViewModelType!
//viewModelをModelTypeで型宣言してあげることで、
//inputs, outputs変数(プロトコル)を通さなければならなくなり、
//イベントがinputsなのかoutputsなのかはっきりする。(重要)

input

let _ = viewModel.inputs.input.onNext(true)
//ViewModelにイベントを送りたいときは、onNextメソッドを使う。
//画面更新が入る時など

output

let _ = viewModel.outputs.output
    .subscribe(onNext: { [weak self] bool in
    //viewModelからのイベント結果を監視し、イベントがきたら発火する
    //weak selfはメモリリーク対策
        guard let self = self else {return}
        //guard let もメモリリーク対策
        //イベント結果がきた時にしたい処理を書く
     }).disposed(by: disposedBag)
//disposedBagを入れることで、購読の重複を防ぐ。
//(詳しくは調べてください)

遷移

スクリーンショット 2021-01-25 16.33.17.png

instantiateライブラリを使っているので、遷移の仕方は少し特殊
この場合はnavigationControllerを使ったパターン。

まとめ

RxSwift、難しいですよね。
さらっとやったらあとは実務でゆっくり身に付けるのが一番効率が良さそうです。

ちなみに、RxSwiftの参考書なら RxSwift研究読本がおすすめです。

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

[Swift]数字の配列から合計(sum)と平均(average)を計算

swiftで数字の配列[1, 2, 3, 4, 5]などから合計と平均を出す機会が何度かあったのですが、毎回調べて実装していたので、まとめてみました。

Array.reduceを使う

数字の配列の合計を計算する場合、愚直にforEachを使う方法もありますが、Array.reduceを使うと簡潔に書けます。

[1, 2, 3, 4].reduce(0, +)
=>結果は10

第1引数は初期値(今回は0)です。
第2引数はオペレーターです。+-*/の4つが使えます。

参考
https://developer.apple.com/documentation/swift/array/2298686-reduce

extensionを使い、合計と平均を簡単に計算する

Int配列を例に、合計と平均を出すmethodを定義します。
Arrayのベース

extension Collection where Element == Int {

    func sum() -> Int {
        return reduce(0, +)
    }

    func average() -> Double {
        let value = sum()
        return Double(value) / Double(count)
    }

}

ソース

Intだけでなく、Float, Doubleの配列についてもextensionにしたソースを以下に置きました。
https://gist.github.com/usk2000/230d46869c9eba401978a8865d2f66fa

終わりに

今回はよく使う合計と平均について書いてみましたが、もっと数学的なものも書いてみたいですね。

参考

https://qiita.com/motokiee/items/cf83b22cb34921580a52
https://developer.apple.com/documentation/swift/array/2298686-reduce

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

【Swift】AutoLayoutでUI部品のサイズを比率で制約したい

はじめに

現在、Swiftの学習を初めて一ヶ月ほどになり、ちょっとしたアプリを作ってみたりしています。
そこでデバイスサイズにより、レイアウトが崩れてしまうという壁にあたりました。

各デバイスの画面サイズに対応できるようAutoLayoutに挑戦しましたが全然わかりませんでした。
試行錯誤していくうちに、自分の実現したかったUI部品のサイズが画面の高さの比率で変化するやり方をなんとなく理解したのでここに記録します。(間違えていたり、誤認識している場合ご指摘お願いいたします。)

開発環境

  • macOS Catalina version10.15.7
  • Xcode version12.2
  • Swift5

AutoLayoutについて

こちらの記事が大変わかりやすく参考になりました。
怖くない!AutoLayout 〜多画面対応 with Storyboard〜

AutoLayoutではいろいろ設定ができるため、自分のような初学者には難しいので今回必要なもののみに焦点を当てます。

今回の重要なもの

こちらの二つ

  • Spacing to nearest neighbor
  • Equal Heights

Spacing to nearest neighborは直訳すると「最近傍への間隔」ということで近くにある部品との距離を指定することのできるものです。下記画像の箇所で指定します。
各項目の詳細については先ほどの記事を参考にしてください。

スクリーンショット 2021-01-24 14.53.10.png

二つ目に重要なものもはこの画像に写っている、Equal Heightsです。
Equal Heightsは2つ以上のUI部品を選択した状態のときに使用することができる設定で、選択した複数部品の高さを同じ値に揃えることができます。

これを用いて、画面高さとUI部品をEqualHeightsで設定してあげれば良い。
でも、画面高さと同じだと全然使えないじゃん!と自分は思いました。

安心してください。今回で言えば、画面高さに対して比率でサイズを指定することができます
これを使うことで、画面高さに対して常に50%の大きさにしたい!など実現できます。
※同様にEqualWidthsを使うことで、横幅も比率指定できます!

やってみる

まずは適当にUI部品(部品Aとします)を設置します。この部品AをSafeArea高さに対して25%の高さにしたいと思います。

スクリーンショット 2021-01-24 15.10.54.png

次に、Spacing to nearest neighborを使って、画面上部と左右に対して距離を0とします。
下記画像のように指定したら、Add 3 Constraintsを押すことで適用されます。

スクリーンショット 2021-01-24 15.14.29.png

そうすると、このようにSafeAreaの上部と左右の距離が0、つまり部品Aの幅=デバイスの幅となります。左の階層部分をみると、Constraintsというものが追加されています。
これがAutoLayoutで設定した制約です。ここをクリックして設定値を変えたりできます。

スクリーンショット 2021-01-24 15.16.58.png

次に、Equal Heightsを用いてSafeAreaと部品Aの高さを同じになるよう設定します。
やり方は、左の階層部分のSafe Area部品Aを選択(Macならcommandを押しながらクリック)し、Equal Heightsを設定します。そうするとこのようになります。

スクリーンショット 2021-01-24 15.25.26.png

部品AがSafeAreaと同じ高さになったため、このように画面いっぱいに表示されます。
また、階層部分に部品A(25%).height = Safe Area.heightという制約が追加されました。

最後にやることは、比率の設定です。今回はSafeAreaに対して25%の高さにします。先ほど追加された部品A(25%).height = Safe Area.heightという制約を選択します。

そうすると、下記画像のようにEqual Heights Constraintが表示されるのでMultiplierの値をみると1となっています。これは、部品AがSafeAreaの高さに対して1.0(100%)の高さ、つまり同じ高さということになっている。この値を0.25(25%)と設定すればOK。

スクリーンショット 2021-01-24 15.32.01.png

スクリーンショット 2021-01-24 15.36.26.png

無事、SafeAreaに対して25%の高さに設定することができました。
制約部分を見てみると、部品A(25%).height = 0.25 × Safe Area.heightとしっかり設定ができていることが確認できます。高さの比率を変えたい場合は、先ほどと同様の手順で値を変えることで変更できます。

同様の手順で、他の部品も設置してみましょう。
今回は25%/45%/30%と分けて設置してみました。他のデバイスサイズでも確認してみます。

スクリーンショット 2021-01-24 15.44.56.png
iPhone11

スクリーンショット 2021-01-24 15.45.20.png
iPhone4S

こんな感じで、画面サイズが変わっても指定の比率通りにサイズが変更されています。

まとめ

今回は、AutoLayoutを用いて、UI部品のサイズを比率で制約しました。
引き続きいろいろ触ってみて、AutoLayoutを意のままに操れるよう頑張ります。

以上。

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

Realm Studioを使う途中で【エラー対処】Opening Realm files of format version 20 is not supported by this version of Realm

RealmStudioの使い方

RealmStudioを起動し Open Realm file からRealmファイルを開くことができます。
RealmファイルはAppDataのDocumentフォルダーの中にdefault.realmファイルが存在すると思います。
→参考記事 AppDataの参考記事

しかし、エラー発生?

Opening Realm files of format version 20 is not supported by this version of Realm

解決方法

Githubのissue

Opening Realm files of format version 20 is not supported by this version of Realm · Issue #6893 · realm/realm-cocoa

Realm Studio リリースページ

Realm Studio Releases

ここから最新版をインストールすることでRealmファイルを開くことができます。

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