20200315のSwiftに関する記事は12件です。

UIToolbarをコードで実装する方法(iPhone8,iPhone11対応)

1.はじめに

アプリ作成のために、UIToolbarが必要になったのですが、ストーリーボードを使わない方法で作成した場合、
昔の記事はSafeAreaという概念が無かったiPhoneX以前のものが多く、実装に苦労したので備忘録として掲載します。
(もし間違いがございましたら、遠慮なく指摘していただくと幸いです)

2.環境

Xcode Version 11.3.1

参考記事
Custom UIToolbar too close to the home indicator on iPhone X

3. タブバーの高さを直打ちで入力した場合の問題

私が検索した限り、タブバーの高さは直打ちで決めていることが多かったように感じます。

例えば、こんな感じです。

コードを直打ちした場合、
import UIKit

class testVC: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBlue
        configureToolBar()
    }


    func configureToolBar() {

        // ツールバーの高さを直打ち
        let footerBarHeight: CGFloat = 100

        // ツールバーのインスタンス化
        let toolbar = UIToolbar(frame: CGRect(
            x: 0,
            y: self.view.bounds.size.height - footerBarHeight,
            width: self.view.bounds.size.width,
            height: footerBarHeight)
        )

        toolbar.barStyle = .black

        //戻るボタンの実装
        let backButton = UIButton(frame: CGRect(x: 0, y:0, width: 100, height: 40))
        backButton.setTitle("Back", for: .normal)
        backButton.addTarget(self, action: #selector(back), for: .touchUpInside)
        let backButtonItem = UIBarButtonItem(customView: backButton)

        //ボタンを左右に分けるためのスペースの実装
        let flexibleItem = UIBarButtonItem(barButtonSystemItem: .flexibleSpace,
        target: nil, action: nil)

        //進むボタンの実装
        let nextButton = UIButton(frame: CGRect(x: 0, y:0, width: 100, height: 100))
        nextButton.setTitle("Next", for: .normal)
        nextButton.addTarget(self, action: #selector(goToNext), for: .touchUpInside)
        let nextButtonItem = UIBarButtonItem(customView: nextButton)

        // ツールバーにアイテムを追加する.
        toolbar.items = [backButtonItem,flexibleItem,nextButtonItem]

        self.view.addSubview(toolbar)
    }

    // 戻るボタンをクリックした時の処理
    @objc func back() {
        print("戻るボタンがクリックされた")
    }

    // 進むボタンをクリックした時の処理
    @objc func goToNext() {
        print("進むボタンがクリックされた")
    }
}

ここでの

let footerBarHeight: CGFloat = 100

のように直打ちしてしまうと、下記の画像のように、
Appleのガイドラインには準拠していませんし、見た目もイマイチです。
toolBar_qiita.001.png

4. SafeAreaLayoutGuideを使用した場合

この問題を解消するにあたり、SafeAreaLayoutGuideを使ったところ、下記の画像のようになりました。

       NSLayoutConstraint.activate([
            toolbar.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
            toolbar.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
            toolbar.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
            toolbar.heightAnchor.constraint(equalToConstant: 50)
        ]) 

toolBar_qiita.002.png

なお、コードはこのようになります。

SafeAreaLayoutGuideを使用した場合
import UIKit

class SelectPostImageVC: UIViewController {

    let toolbar = UIToolbar ()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBlue
        configureToolbar()
    }


    func configureToolbar() {
        self.view.addSubview(toolbar)
        toolbar.barStyle = .black
        toolbar.translatesAutoresizingMaskIntoConstraints = false

        //戻るボタンの実装
        let backButton = UIButton(frame: CGRect(x: 0, y:0, width: 100, height: 100))
        backButton.setTitle("Back", for: .normal)
        backButton.addTarget(self, action: #selector(back), for: .touchUpInside)
        let backButtonItem = UIBarButtonItem(customView: backButton)

        //ボタンを左右に分けるためのスペースの実装
        let flexibleItem = UIBarButtonItem(barButtonSystemItem: .flexibleSpace,
        target: nil, action: nil)

        //進むボタンの実装
        let nextButton = UIButton(frame: CGRect(x: 0, y:0, width: 100, height: 100))
        nextButton.setTitle("Next", for: .normal)
        nextButton.addTarget(self, action: #selector(goToNext), for: .touchUpInside)
        let nextButtonItem = UIBarButtonItem(customView: nextButton)

        // ツールバーにアイテムを追加する.
        toolbar.items = [backButtonItem,flexibleItem,nextButtonItem]

        self.view.addSubview(toolbar)

        //ここでSafeAreaLayoutGuideを使用
        NSLayoutConstraint.activate([
          toolbar.bottomAnchor.constraint(equalTo:view.safeAreaLayoutGuide.bottomAnchor),
          toolbar.leadingAnchor.constraint(equalTo:view.safeAreaLayoutGuide.leadingAnchor),
          toolbar.trailingAnchor.constraint(equalTo:view.safeAreaLayoutGuide.trailingAnchor),
          toolbar.heightAnchor.constraint(equalToConstant: 50)
        ])
    }

      // 戻るボタンをクリックした時の処理
       @objc func back() {
           print("戻るボタンがクリックされた")
       }

       // 進むボタンをクリックした時の処理
       @objc func goToNext() {
           print("進むボタンがクリックされた")
       }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

シワ(Sign in with Apple) 対応

Sign in with Appleとは

Apple ID を利用したサインイン方法。
サードパーティーログインを使用しているアプリは2020/04までにSign in with Appleに対応する様に義務化した。

App Storeに提出されるすべての新しいAppやアップデートは、2020年4月30日までにこれらのガイドラインに従う必要があります。
引用元:https://developer.apple.com/jp/news/?id=03042020d

そうです。いつものAppleの無茶振りです。
対応しないとリジェクトされてしまう為、仕方なく実装する事に、、
まずは仕様から

アプリへの公開情報

アプリに公開される情報は大きく分けて以下の2つです。

  • 名前
  • メールアドレス
    • 公開メールアドレス
    • プライベートメールアドレス

これらの値はSign in with Apple新規登録時かユーザーがアプリと紐付け解除を行った後の初回のみ設定する事が出来ます。

名前

アプリに連携する名前はアプリ毎にユーザーが設定することができます。
デフォルトではiCloud アカウントに設定している姓名が入ってきますが、公開したくない場合は任意の名前に変更可能になっています。

公開メールアドレス

ユーザーがiCloudの登録に使用しているメールアドレス。
* 実際のメールアドレスなので転送を停止する機能などは今回対象外

プライベートメールアドレス

認証時にユーザーがメールアドレス(Apple ID)を非公開設定すると、Appleが自動生成したランダムな英数字のメールアドレスをアプリ(サービス)に連携する。その結果ユーザーは自身のメールアドレスを公開する事なくアプリでログインが可能になる。
* アプリとのメールやりとりはAppleが アプリ→自動生成メールアドレス→プライベートメール という形でメールを転送してくれる。(設定の必要あり)
* ユーザーはメールの転送を停止したり、転送先のメールアドレスを変更する事が出来る。

このプライベートメールアドレスは出来るとユーザーにとって何が嬉しいのか?
サービス登録時に実際のメールアドレスを使用するとサービスの利用を停止した後にもメールアドレスを保持されて悪用されるリスクがある。そこでこのプライベートメールアドレスを使用すると実際のメールアドレスはサービスに公開せず、利用停止したい場合には紐付け解除(プライベートメールアドレスの破棄)を行えば悪用されなくなるというメリットがある。

アプリとの紐付けの解除

iCloudの管理画面からアプリとの紐付け解除を行うことが出来る。
ユーザーにとって

※iOS13以上では[設定>Apple ID(一番上の箇所)>パスワードとセキュリティ>Apple IDを使用中のApp]からでも解除出来る

CSRF対策

Sign in with AppleはCSRF対策の為にAppleの認証APIを実行する際にstateを指定することができる。
簡単に言うとなりすまし防止の為にアプリで送信時に作成したstateとAppleの認証時に返されたstateが一致しているかを確認する。

完璧に仕様が分かったことで対応する事を以下に記載。

対応内容

あくまでアプリが対応する内容を記載する。

  • コーディング
    • iOS13以上の対応
    • iOS12未満の対応(必須ではない)
    • Android対応(必須ではない)
  • Apple Developerでの設定
    • Sign in with Appleの有効化
    • メールのホワイトリスト追加
    • ServiceIDの登録
  • Xcodeの設定
    • Sign in with Apple有効化
  • デザイン対応
    • ガイドラインに沿ったデザイン

iOS13での対応

Appleのログインボタンを生成する。

LoginViewController.swift
    func setupProviderLoginView() {
        let authorizationButton = ASAuthorizationAppleIDButton()
        authorizationButton.addTarget(self, action: #selector(handleAuthorizationAppleIDButtonPress), for: .touchUpInside)
        self.loginProviderStackView.addArrangedSubview(authorizationButton)
    }

ユーザーの姓名とメールアドレスを受け取る様な認証のリクエストをAppleに対して行う。

LoginViewController.swift
    @objc
    func handleAuthorizationAppleIDButtonPress() {
        let appleIDProvider = ASAuthorizationAppleIDProvider()
        let request = appleIDProvider.createRequest()
        request.requestedScopes = [.fullName, .email]

        let authorizationController = ASAuthorizationController(authorizationRequests: [request])
        authorizationController.delegate = self
        authorizationController.presentationContextProvider = self
        authorizationController.performRequests()
    }

Delegateで結果を受信する。

LoginViewController.swift
extension LoginViewController: ASAuthorizationControllerDelegate {
    func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
        if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {
            // 成功した場合       
            let userIdentifier = appleIDCredential.user
            let fullName = appleIDCredential.fullName
            let email = appleIDCredential.email
        }
    }

    func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
        // エラーが発生した場合
    }
}

iOS12以下での対応

WebViewを利用してApple JSに対応する必要があります。
サーバサイドの実装が必要だったりAppleボタンを自作しなければならなかったりするので
出来ればiOS13以降のみの対応の方が無難だと思います。

デザイン対応

Appleのガイドラインの記載を抜粋。
* Appleのボタンを目立つようにする
* 他サードパーティのログインボタンよりも小さくしないこと
* スクロールしないでもAppleのボタンが表示されるようにする
* ボタンの色は背景色と被らないようにする
などなど

ガイドラインに従わないとリジェクトされます。
実際に初めてSign in with Apple対応した時にはボタンと背景の色やフォントサイズなどでリジェクトされました。
(iOS12対応の為にボタンは自作しました)

https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple/overview/

まとめ

iOS12以下に対応する為には、自作したボタンをAppleのデザインに合わせたり認証をWebViewで行ったりしないとならなく別対応が必要なので骨が折れますが、
iOS13のみなら基本Appleが提供しているSDKを使用すれば比較的簡単に開発が済みます。
最近(2020/03時点)リジェクトが厳しくなっている気がするのでデザインのガイドラインなどはしっかり読んで対応した方が良いです。

参考URL

* サンプルコード
https://developer.apple.com/documentation/authenticationservices/adding_the_sign_in_with_apple_flow_to_your_app

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

ライブラリ作ったから見てくれ

申し訳ありません。「見て下さい。」ですね。
調子乗りました。

気を取り直して

こんにちはこんばんは。初投稿のいるべです。
昨年クリスマスに独り身でしにたくなったのでswiftの勉強を始めました。
情報系の学部にいながらプログラミングとは関わらない生活をしていたのでプログラミング歴は実質3ヶ月程となります。

今、自分のお気に入りのものを教えてよって主旨のアプリを作成してます。SNSは素人には無理ゲーとか言う声が聞こえてきそうですがまぁやるだけタダなので黙ってて下さい。

今回は表題の通り。そのアプリを作る上で欲しくなった機能をライブラリとして作成したので、「まぁ見てって下さいよ」というところでつらつらと書かせて下さい。
ただの宣伝にならないように色々と書けたらいいなと思います。

まず前提。なんでこんなものを作ったか、だ。

いやいや、その「こんなもの」を先に言えよと思う方もいらっしゃるかも知れんがスルーします。

UICollectionViewって意味分かんなくね?

玄人の皆様方に置かれましては何が?と思われるかも知れないが、
- まずdelegateが意味わからん。
- flowlayout何それ??
- そもそもRunさせても何も表示されん、、
その他壁は沢山あると言っても過言では無い。(断言)
少なからず全く調べずにcollectionViewを使って意図した挙動を実装することができる人はそう多く無いのではと考えている(え?みんなふつーに書けるの、、?)

みんな悩んでるやろし作ったろ!

まぁもちろん既に上位互換ライブラリは存在していますが経験です。沢山勉強になりました。
僕と同じような初心者の皆様方も、「既にあるからそれ使おう」「既にあるから作るのやめよう」と言うのは悪くも無いけど良くも無いことがあるということを覚えておきましょう。たまには遠回りも必要ということです。

お待たせたしました。

今回作ったものはこちらになります。
CheckableTagImage.jpg
と画像をのっけてもインパクトがない。。
真ん中下のネコを妹に描いてもらった。ほんの1分程度でさらっと。すごい。

タグを簡単に生成したいやんってことでCheckableTagというライブラリを作成しました。

簡単に説明しますと、簡単にタグを作成できるライブラリです。
更に、タップしたら色が変わるようになっているのでチェックボックスのような使い方もできます。 
画像を見ての通り、タグの形にもいくつかあるので選ぶのに悩んじゃいますね?(☝︎ ՞ਊ ՞)☝︎

導入

現在はPodのみの対応です。

pod 'CheckableTag'
を追加してpod installして下さい。

使い方

import UIKit
import CheckableTag

class ViewController: UIViewController {

    let items = ["Hello"]

    let tagView: CheckableTag = {
        let view = CheckableTag()
        return view
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

        tagView.frame = self.view.bounds
        self.view.addSubview(tagView)
        tagView.dataSource = self

    }
}

extension ViewController: CheckableTagDataSource {
    func getSelected(sender: CheckableTag) -> [Bool]? {
        return nil
    }
    func getItems(sender: CheckableTag) -> [String] {
        return items
    }
}

最低限のコードはこれだけです。
これで、下のような感じになります。

IMG_3691.jpg

*トリミングしてあります。
タップすると、
IMG_3692.jpg
色が変わります。

プロパティ

まぁここは興味があれば自由に弄ってみて下さい。
いるべ的には下記の設定が好きです。

//tagの形を変更
tagView.cellType = .curve
//tagの表示スタイルを変更
tagView.cellStyle = .groove
//ユーザが選択可能かどうか指定
tagView.canSelected = false
//フォントサイズの指定。fontsizeによってtagのサイズが変わる。
tagView.fontSize = 20

色の変更

/// 選択状態の色を指定する
/// - Parameters:
///   - text: テキストカラー
///   - back: バックグラウンドカラー
///   - line: 枠線カラー
tagView.setSelectedColor(text: .white, back: .red, line: .black)

/// 非選択状態の色を指定する
/// - Parameters:
///   - text: テキストカラー
///   - back: バックグラウンドカラー
///   - line: 枠線カラー
tagView.setUnSelectedColor(text: .gray, back: .white, line: .gray)

選択状態の取得

//存在する全てのタグの選択状態を取得します。
//trueのとき選択されている
//falseのとき非選択状態。
let isSelectedArray: [Bool] = tagView.getIsSelected()

細かいことを指定する場合

DataSourceの使用例

tagView.dataSource = self

------------------------------

extension ViewController: CheckableTagDataSource {
    ///初期選択状態を指定できる。
    ///canSelect = falseにしてこれと組み合わせればタグになる。
    func getSelected(sender: CheckableTag) -> [Bool]? {
        return [true, false, true, false, true, true]
    }

    ///tagを生成するためにタグのテキストを伝えられる。
    func getItems(sender: CheckableTag) -> [String] {
        return items
    }
}

Delegateの使用例

tagView.delegate = self

----------------------------

extension ViewController: CheckableTagDelegate {
    ///セルを選択したときの動作を設定できる。
    func didSelected(cell: CheckableCellProtocol) {
        print(cell.textLabel.text)
    }
}

Animationの使用例

tagView.animation = self

----------------------------

///このアニメーションは意味不明なのでただの参考として扱って下さい。
extension ViewController: TouchCellAnimationProtocol {
    ///タップし始めのときに行われるアニメーション。
    func touchStartAnimation(cell: CheckableCellProtocol) {
        UIView.transition(with: cell, duration: 1.0, options: [.transitionFlipFromTop], animations: nil, completion: nil)
    }
    ///タップ終わりのときに行われるアニメーション。
    func touchEndAnimation(cell: CheckableCellProtocol) {
        UIView.transition(with: cell, duration: 1.0, options: [.transitionFlipFromBottom], animations: nil, completion: nil)
    }
}

選択状態について

collectionViewやtableViewの選択状態を管理する時にスクロールしたら選択状態がずれていることがあります。
これはセルごとで選択状態を管理するのではなく、セル全ての選択状態を管理する配列を用意してそれを読んであげればそんな問題とはおさらばです。

///collectionviewを使っている上位view
    //cellに代入するテキストの配列。
    private(set) var items: [String] = []
    //cellの選択状態を管理する配列
    public lazy var isSelectedItems: [Bool] = {
        return [Bool].init(repeating: false, count: items.count)
    }()
///cellが生成されるタイミング
/// UICollectionViewDataSourceを継承

public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    //cellを再利用するために引っ張り出してくる
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)

    //選択状態を管理している配列から、今扱っているcellに該当する選択状態を見て、それに見合ったスタイルを反映する。
    if isSelectedItems[index.row] {
        cell.selectedColor(text: textColor, back: backColor, line: lineColor)
    } else {
        cell.unSelectedColor(text: textColor, back: backColor, line: lineColor)
    }

    return cell
}

Enumは便利。

Enumってすごいですね。僕は気持ちが先走ってよくタイポをしてしまう人間なのですが,
予測変換で出てこればタイポもないし、後からの要素の追加も楽々!今回はEnumと後述するProtocolに救われました。

public enum CellType: String {
    case square = "SquareCheckableCell"
    case curve = "CurveCheckableCell"
    case round = "RoundCheckableCell"
    case circle = "CircleCheckableCell"
}

これは実際のコードです。Stringがうだうだしてますね。
タイポしやすい文字列も
cellType.rawValue

を使えばここで設定している文字列が取得できますからタイポが一気に減ります。

最初はcase squareだけだったんですが、一度一通り作って仕舞えばもう簡単!
case *** と新しい要素をここに追加してビルドします(おい)
もちろんビルドは通りません。

そのビルドに対して出たエラーのところを修正すればあら不思議。
もうまた別の要素が使えるようになっているではないですか。

///指定されたcellのタイプを返す。
public func getCellType(collection: UICollectionView, index: IndexPath) -> CheckableCellProtocol {
        switch cellType {
        case .square:
            return collection.dequeueReusableCell(withReuseIdentifier: cellType.rawValue, for: index) as! SquareCheckableCell
        case .curve:
            return collection.dequeueReusableCell(withReuseIdentifier: cellType.rawValue, for: index) as! CurveCheckableCell
        case .round:
            return collection.dequeueReusableCell(withReuseIdentifier: cellType.rawValue, for: index) as! RoundCheckableCell
        case .circle:
            return collection.dequeueReusableCell(withReuseIdentifier: cellType.rawValue, for: index) as! CircleCheckableCell
        }
    }

今回はご覧の通り似てるけど違うものをいくつも作ったのでstrategyパターンを採用しました。(実は後付け)
これとenumのおかげで今後も変更がだいぶ楽になりそうです。

Protocolくんありがとう。

protocolの何が便利かというと後からの変更がとても楽です。追加も楽です。

protocolで決まりを作っておけば後はボーッとコーディングできる。
import Foundation
import UIKit

public protocol CheckableCellProtocol: UICollectionViewCell {
    ///cellの細かな表示を変更する
    var cellStyle: CellStyle { get set }
    ///cellとcontentViiewの間のマージン
    var margin: CGFloat { get }

    ///collectionviewに最初からあるview
    var contentView: UIView { get }
    ///textを表示するために一番中心に配置されるview
    var textLabel: UILabel { get }

    ///アニメーションを行うためのプロトコル
    var animationProtocol: TouchCellAnimationProtocol! { get set }
    ///labelに表示するテキストをセットする
    func setTextToTextLabel(textName: String)
    ///選択時の色設定
    func selectedColor(text textColor: CellColor?, back backColor: CellColor?, line lineColor: CellColor?)
    ///非選択時の色設定
    func unSelectedColor(text textColor: CellColor?, back backColor: CellColor?, line lineColor: CellColor?)

    ///cellを設定
    func setCell()
    ///normalstyleの設定。cellにぴったりくっつく
    func setNormalStyle()
    ///groovestyleの設定。cellとの間に少し溝を作る
    func setGrooveStyle()
}

///共通してるものはもう作っておく
extension CheckableCellProtocol {

    public var margin: CGFloat {
        return LayoutConstants.margin
    }

    public func setTextToTextLabel(textName: String) {
        textLabel.text = textName
    }

    public func selectedColor(text textColor: CellColor?, back backColor: CellColor?, line lineColor: CellColor?) {
        textLabel.textColor = textColor?.selectedColor ?? .white
        contentView.backgroundColor = backColor?.selectedColor ?? .init(red: 255 / 255, green: 100 / 255, blue: 100 / 255, alpha: 1)
        self.layer.borderColor = lineColor?.selectedColor?.cgColor ?? UIColor.gray.cgColor
    }

    public func unSelectedColor(text textColor: CellColor?, back backColor: CellColor?, line lineColor: CellColor?) {
        textLabel.textColor = textColor?.unSelectedColor ?? .gray
        contentView.backgroundColor = backColor?.unSelectedColor ?? .init(red: 230 / 255, green: 230 / 255, blue: 230 / 255, alpha: 1)
        self.layer.borderColor = lineColor?.unSelectedColor?.cgColor ?? UIColor.gray.cgColor
    }

    public func setCell() {
        switch cellStyle {
        case .normal:
            setNormalStyle()
        case .groove:
            setGrooveStyle()
        }
    }
    public func setNormalStyle() {
        textLabel.frame = contentView.bounds
    }
    public func setGrooveStyle() {
        contentView.frame = CGRect(x: self.bounds.minX + margin, y: self.bounds.minY + margin, width: self.bounds.width - margin * 2, height: self.bounds.height - margin * 2)
        textLabel.frame = contentView.bounds
    }

}

protocolのextensionを使えば、必ず同じな部分の実装を楽に共通化できるのでとても便利。まぁclassを継承しても良いんだけどprotocolは直接インスタンス化できないところが好き。

ストラテジーパターン

ストラテジーパターンを使うと同じようなものをいくつも作成する時に追加が楽。変更も楽。
やり方は、ルール(プロトコル)を作ってそのルールに則って種類の分かれる奴らをそれぞれ作る。今回で言えばCheckableCellProtocolを作ってそれに則ったcellをSquare, Curve, Round, Circleと作った。それらを利用する側は外部からそのプロトコルに則ったインスタンスを貰うかそれを特定できる情報をもらうようにすれば完成。後は簡単に追加削除変更なんでもござれ。
具体的には上記のgetCellType関数が肝になっているのでgithubから見てみてね。

さてこのライブラリの完成度は如何程か?

良かった点

  • 割と使いやすいのでは?
  • 変更しやすかった。楽しくコーディングできた。
  • 自分の理想にかなり近い。

改善点

  • はいまずUITestが通りません。意味がわかりません。調べたところdelegateが動作していないのが原因っぽいのですがだからと言ってどうすれば解決するのかさっぱりわからない。要勉強ですね。
  • delegateでタグ同士の間のスペースとか指定できるようにしたい。
  • github全部ニホンゴ。
  • コードレイアウトマンなのでstoryboardで使えるか確認していない。。(近日中に必ず確認して使えるようにするつもりではあるが)
  • CellTypeもCellStyleももっと種類増やしたい。
  • タグ左寄せの設定もできるようにしたい。

自己評価は120点!良くがんばりました。これからも頑張りましょう!(文句言うな。)

初心者をはじめこの記事を読んで下さった皆様方。

軽い感じで読みやすくしようと思って書いていましたがそうでもなくなってしまいましたね。
もっと色々書きたかったけど難しいし体力切れや。
飛ばし飛ばしでもここまで読んで下さってありがとうございました。解説の部分はあんまり参考にならなかったと思います。定期的にアウトプットしてしっかり言語化することが大事だと感じました。
改善点はまだまだありますがぜひ一度使ってみて下さい。そしてissueやPR投げてくださると嬉しいです。

なぜ「初心者をはじめ」と書いたかと言いますと、このコードは簡単なコードで書かれています。まぁ初心者が書いている+他人のコードなので読みにくいかも知れませんが。もっとプログラミング出来るようになりたいと考えている方は、OSSにPR投げたりしてcontributionを重ねていくことって大事だと思うのでそれのとっかかりとして、まずこのライブラリにPRを投げるのも良いと思うよってことが言いたかったです。
READMEのちょっとした修正やタイポ修正などでもこちらとしては本当に助かりますしいわゆるwin-winですね。(古い?)こんなのどうや?って気軽に投げて下さい。

※この記事の内容は変なこと言っている可能性も大いにあります。情報の取捨選択をうまく行って、僕には文句ではなく指摘をしてくださると踊って喜びます。

長文駄文失礼しました。ありがとうございました。

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

signal SIGABRTの解決策(画面遷移時:segue)

前提として、signal SIGABRTとはシグナルが中断すること。バグです。

この原因はいくつかあるそうで、解決策は一つではありません。ですので、こういった記事をいくつも調べて一つずつ確かめるしかありません。僕もそうしました。。。少し骨が折れます。

今回はその中で僕がsignal SIGABRTをおこした原因と解決策を、一例として簡潔にまとめましたので参考にしていただければと思います。

結論から言いますと、僕の場合原因は二つありました。(だからどれか一つを改善してもうまくいかなくてやり直す、という抜け出せないループにハマってしまっていたのです。)

まず一つ目ですが、segue先のOutletLabelがダブっていて⚠️マークになっていたことです。スクリーンショットする前に訂正してしまったので画像では正常ですが、『Outlets』という欄のlabelがダブっていました。
segue先のLabelのエラーってなんか関係ないと思ってたんですけど、これの直後に解決できたので、関係あったんだと思います。。。(よくわかっていません)
スクリーンショット 2020-03-15 21.01.35.png

二つ目ですが、segueボタンのidentifierを入力していなかったことです。
画像にあるように、segueのボタンを選択した状態で右上のattribute inspectorを選択して、identifierに遷移先のストーリーボード名を入力
スクリーンショット 2020-03-15 21.10.59.png

以上のふたつを実行したら、解決できました。(汗)
皆さんがこの記事の方法で解決できるかはわかりませんが、原因が一つだとは限らないということを知っていただければ、僕のような負のループには陥らないで済むのではないかと思います。

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

swift コードで単色の画像を生成する方法

var image: UIImage? {
        let rect = CGRect(x: 0, y: 0, width: 1, height: 1)
        UIGraphicsBeginImageContext(rect.size)
        guard let context = UIGraphicsGetCurrentContext() else {
            return nil
        }
        context.setFillColor(UIColor(red: 1, green: 1, blue: 1, alpha: 0.8).cgColor)
        context.fill(rect)
        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return image
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

エラーが起きた際に、BreakPointを自動配置してエラー箇所を見つけ出す。

例えば、Thread 1: signal SIGABRT等の抽象的なエラーコードが表示された場合、原因がピンポイントで提示されないのでどんな対策を打てばいいのかわからない時があります。

そんなときは、BreakPointを自動配置させて、原因となるコードをあぶり出しましょう。

僕も今回調べて知ったのですが、方法は簡単でした。

まず、ブレークポイントナビゲータを開きます。

スクリーンショット 2020-03-15 19.04.49.png

そして、左下角にあるプラス『+』ボタンを押し、exception breakpointを押します。

スクリーンショット 2020-03-15 19.05.57.png

これで完了!
こいつのおかげで、間違ったコードにぶち当たった時に、そこでbreakpointを勝手に打って止まってくれます。
そうすれば、そのコードについてのエラー記事を検索できたり、質問投稿しやすくなります。

ありがたい。。

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

NAVITIMEのiOSアプリで使われているライブラリを調査!

NAVITIME

NAVITIME(ナビタイム)は、ナビタイムジャパン社が提供する、電車や車、徒歩などを使った経路を一度に検索できる総合ナビゲーションサービス。
引用先: https://ja.wikipedia.org/wiki/NAVITIME

設定アプリからライブラリのライセンスを確認

AFNetworking

  • ネットワーク通信のライブラリ
  • Alamofireの次に人気のあるHTTP通信ライブラリ

AWS

  • AWSCognito

    • Amazon Cognito を使用すれば、ウェブアプリケーションおよびモバイルアプリに素早く簡単にユーザーのサインアップ/サインインおよびアクセスコントロールの機能を追加できます。Amazon Cognito は、数百万人のユーザーにスケールし、Facebook、Google、Amazon などのソーシャル ID プロバイダー、および SAML 2.0 によるエンタープライズ ID プロバイダーを使用したサインインをサポートします。 引用先: https://aws.amazon.com/jp/cognito
  • AWSCore

    • AWSのCoreライブラリ
    • 共通的な処理が入っているそうです。
  • AWSKinesis

    • Amazon Kinesis でストリーミングデータをリアルタイムで収集、処理、分析することが簡単になるため、インサイトを適時に取得して新しい情報に迅速に対応できます。Amazon Kinesis は、アプリケーションの要件に最適なツールを柔軟に選択できるだけでなく、あらゆる規模のストリーミングデータをコスト効率良く処理するための主要機能を提供します。Amazon Kinesis をお使いになると、機械学習、分析、その他のアプリケーションに用いる動画、音声、アプリケーションログ、ウェブサイトのクリックストリーム、IoT テレメトリーデータをリアルタイムで取り込めます。Amazon Kinesis はデータを受信するとすぐに処理および分析を行うため、すべてのデータを収集するのを待たずに処理を開始して直ちに応答することが可能です。 引用先: https://aws.amazon.com/jp/kinesis/

Firebase

  • Crashlytics、Fabric
    • Firebaseのクラッシュ解析ツールです
    • ほとんどのアプリに入っていることが多いです。

FrameAccessor

  • Frameのめんどくさい処理を省略できるライブラリです。
before
CGRect newFrame = view.frame;
newFrame.origin.x = 15.;
newFrame.size.width = 167.;
view.frame = newFrame;
after
view.x = 15.;
view.width = 167.;
instead of

GTMSessionFetcher

  • HTTP通信の拡張ライブラリです。
  • Googleが出しているライブラリですがStarが123と少なめ。

HMSegmentedControl

  • UISegmentedControlの拡張ライブラリです。

adjust

リアルタイムに売上、イベント数、セッション数、インストール数、クリック数といった定量データに加え、コホート分析に基づいて、広告がどのような効果を可視化するためのサービスです。

LicensePlist

  • 設定アプリでライセンスを一覧化してくれるライブラリです。

Quick

  • iOSのテスト用フレームワークです。
  • XCTestとよく比較されますね。

Protobuf

Repro

  • Reproは従来型の定量分析に加え、各種分析機能からユーザーをターゲティングして、プッシュ通知やメッセージを配信できる機能を提供するアプリとWebサイトの成長支援プラットフォームです。 引用先: https://docs.repro.io/ja/

SwiftyBeaver

TinyConstraints

  • Auto Layoutをめっちゃ書きやすくしたライブラリです。
  • 個人的にも普段から使っています。

Toast

  • 簡単にトースト通知を表示することができるライブラリです。

Arm Treasure Data

最後に

今回はNAVITIMEをライブラリを調査してみました!!ライブラリの言語をみるとObjective-Cで開発されたものが多いので、NAVITIMEもObjective-Cで開発されているのではないでしょうか。
次回はSwiftで開発されているアプリのライブラリ調査をしてみたいと思います。
ありがとうございました!!

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

ブラウザアプリ作り(1) とりあえず動く状態にする

前回記事 iOSアプリを作って公開してみたいと思う
Youtubeなどの動画を観ながら別の作業をしたいことも多い訳です。よくある場面としては実況動画を見ながら関連する情報を検索する、というのがあります。あるいはTwitterのTLを見ながらYoutubeの動画音楽を再生したいということもありますよね(YoutubeMusicに課金するのはなんか悔しい)。そこで、これらのニーズに応えられるブラウザアプリを作っていきたいと思います。

今回進んだのは以下です。
・WKWebViewを使った2画面ブラウザ
・セーフエリアの設定
・動画のブラウザ内再生

まず、ブラウザの実装です。
以下のサイトを参考にさせていただきました。
SwiftUIでWebViewを使う

コードは上Qiita記事の円コピなので省略します。WebView.swiftを新しく作成して、ContentView.swiftでWebView(loadUrl:)で呼び出すんですね。非常にわかりやすいです。
スクリーンショット

次に、画面を分割してみます。
VStackでビューを2つ並べてみます。
スクリーンショット

いい感じです。
次に、高さを調整します。Youtubeには画面の3割を、残りを他に割きます。
参考:SwiftUIの肝となるGeometryReaderについて理解を深める

ContentView.swift
struct ContentView: View {
    var body: some View {
    GeometryReader {geometry in
      VStack{
            WebView(loadUrl: "https://www.twitter.com")
              .frame(width: geometry.size.width, height: geometry.size.height * 7 / 10)
              WebView(loadUrl: "https://www.youtube.com")
        }
    }
  }
}

スクリーンショット
画面を7:3で分けられました。

下のセーフエリアがもったいないので、ここも使えるようにします。VStackに

.edgesIgnoringSafeArea(.bottom)

をつけてやります。

最後に、デフォルトだとYoutubeを再生し始めると全画面になってしまうのですが、これが起きないようにします。

参考:How can I add web view preferences to a swift ui web view

変更するのは先ほど作成したWebView.swiftです。

WebView.swift
import SwiftUI
import WebKit

struct WebView: UIViewRepresentable {
    var loadUrl:String
    var allowsInlineMediaPlayback: Bool{true}

    func makeUIView(context: Context) -> WKWebView {
      let webConfiguration = WKWebViewConfiguration()
      webConfiguration.allowsInlineMediaPlayback = true
      return WKWebView(frame: .zero, configuration: webConfiguration)
    }

    func updateUIView(_ uiView: WKWebView, context: Context) {
        uiView.load(URLRequest(url: URL(string: loadUrl)!))
    }
}

スクリーンショット

これで、ひとまず使える当初の目的にかなったアプリにはなりました。
まだまだ機能が足りていないので、随時勉強&追加していきます。

今回作成したコード
https://github.com/KanikaniYou/MultiTube

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

iPhoneで取得可能な磁気の種類

Core Motion

CMMotionManagers > CMMagnetometerクラス > magneticFieldプロパティ

地球の磁場 + 周囲の磁場 + デバイスのバイアス

  • 磁力計からの生のデータを読み取る
  • デバイス自体で生成される磁場の影響を大きく受ける
  • 希土類磁石を近づけない限り、何にも反応しない。デバイス内の磁場は、局所的な外部の磁場や地球の磁場よりもはるかに重視される。

CMDeviceMotion > CMCalibratedMagneticFieldプロパティ

地球の磁場 + 周囲の磁場 - デバイスバイアス

  • デバイスバイアス(オンボード磁場)に対して補正された磁力計の読み取り値
  • iPhone内部の磁場の影響の除去

CoreLocation

CLLocationManager > CLHeading

地球の磁場 - 周囲の磁場 - デバイスバイアス

  • フィルタリングしてローカルの外部磁場を除去
  • 多くの種類の電子機器に見られる固定磁石から発生する磁場など、局所的な磁場の影響を受ける可能性がある。
  • 局所的な外部磁場というのに、建物の鉄骨による影響の除去も含まれる。
  • 他のセンサーからのデータを使用して、磁力計データを安定させてる。
  • 磁場 x、y、zの生のデータは得られない。

参考

In iOS, what is the difference between the Magnetic Field values from the Core Location and Core Motion frameworks? - Stack Overflow

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

Swiftでfor文の最後だけ処理をする

for (i, message) in messages.enumerated() {
    // 全部にしたい処理
    if (i == messages.endIndex - 1) {
        // 最後だけしたい処理
    }
}

まずenumerated()を使うことでi(ここでindexでなく敢えてiを使ったのはenumerated()で取得できるのはただのカウントだから)を取得できるが、逆にendIndexは配列しか持っていない。

Swift3.0からfor文が使えなくなりfor-in文かforEach、もしくはmapなどしか使えなくなった。なので上記のenumurated()を使った「何回目の処理か」を取得する方法が散見されるが、これはindexでないというのが正しい説明のよう。

https://qiita.com/motokiee/items/4886b603631a7c076f46
https://qiita.com/a-beco/items/0fcfa69cca20a0ba601c

messages.countでも同じ値が帰ってくるはずだが、あれ、-1であってるんだっけ…。

guardを使わなかったのは、処理を抜ける時にbreakreturnの違いもあってこんがらがりそうだったから。breakは今の繰り返し処理を抜けて次の繰り返しを始めるが、returnを使っちゃうとfor-inの処理がそこですべて終了しちゃう。

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

[Swift] 同一ドメインでのユニバーサルリンク(Universal Links)は動作しないので注意メモ

Web側とiOS側でユニバーサルリンクを設定したが何故か動作せず、調べたら同一ドメイン上では機能してくれない様でした。。
実装前の調査不足ということで、備忘録的なメモになります。

概要

  • 同一ドメインではユニバーサルリンクは機能しない(アプリが起動しない)
  • サブドメインを使った代替策

同一ドメイン上ではアプリへ遷移ができない

ユニバーサルリンクは同一ドメイン上では動作しないため注意が必要です。
公式ドキュメントでも注意点として記載されていました。。
すでにWebで利用しているユーザーに対して急にアプリ起動したらUX的によろしくないのでAppleさんが制御しているらしいです。
(翻訳した自分の解釈が間違っていなければ)

Support Universal Links:
https://developer.apple.com/library/archive/documentation/General/Conceptual/AppSearch/UniversalLinks.html

サブドメインをリンク先に設定することで、ユニバーサルリンクを有効にすることができます。
とはいえ、サブドメインで分けるほどのページではないがユニバーサルリンクを有効にしたい場面があるかと思います。
次では、見た目上は同一ドメインでユニバーサルリンクが動作する例を紹介します。

代替策

同一ドメインではユニバーサルリンクは無効ですが、サブドメインなどに遷移先を設定すれば有効になります。
以下例では、見た目上は同一ドメインでの遷移ですがユニバーサルリンクをサブドメインとしている代替策になります。

# shop一覧画面(www.hoge.com)から詳細画面を開く場面を想定しています。
# 詳細画面のリンク先をサブドメイン(shop.hoge.com/shop_id)として設定。
# リンク先のサブドメインはリダイレクト先として詳細画面(www.hoge.com/shop/shop_id)を設定。
# 設定したリンクはユニバーサルリンクとして設定しているため、アプリが起動します。
# アプリ未インストール&Webでの表示では、リダイレクト先の画面(www.hoge.com/shop/shop_id)が表示されます。

image.png

まとめ

  • ユニバーサルリンクは同一ドメインでは機能しない
  • 機能させるためには、サブドメインなど活用する(場面に応じてリダイレクト先を設定するなど対応する)
  • ユニバーサルリンクを使用しない方法(URLスキーム)で実装できないかを考える

記載した情報に誤り等ございましたら、ご指摘をいただけると幸いですmm

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

Swift UI で SearchBar を使う

Search Bar をつけたい

SwiftUI でUISearchBarをつけようとすると、存外に苦労しました。おそらくまだ十分にAPIが開放されていないためだと思います。こういう場合、UIViewControllerRepresentableなどを使い、SwiftUIで使えるようにしていくしかないのかなぁと思います。やりたいのは以下の通り、UIKit使ってたら普通にできたよね・・・?
p.png

tl;dr

  • SearchBarを使いたい時はUINavigationControllerをUIViewControllerRepresentableでラップする。その際にUISearchControllerをセット
  • NavigationBarしか使用しない場合は、親ViewのSafeAreaをedgesIgnoringSafeArea(.vertical)で調整
  • TabBarをでNavigationBarを囲っている場合は、親ViewのSafeAreaをedgesIgnoringSafeArea(.top)で調整後、NavigationBarが実装されているViewのSafeAreaをedgesIgnoringSafeArea(.vertical)で調整する

1. SearchBarをSwiftUIで使用する

  • まずUINavigationViewControllerにUISearchControllerを追加して、それををSwiftUIで使用できるようにします。
struct NavigationViewControllerRepresentation: UIViewControllerRepresentable {
    typealias UIViewControllerType = UINavigationController

    func makeUIViewController(context: Context) -> UINavigationController {
        let viewController = UIHostingController(rootView: List{
            Text("one").frame(height: 120.0)
            ...
            Text("six").frame(height: 120.0)
        }.navigationBarTitle("Title"))

        let navigationController = UINavigationController(rootViewController: viewController)
        navigationController.navigationBar.prefersLargeTitles = true

        viewController.navigationItem.searchController = UISearchController()
        return navigationController
    }

    func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {

    }
}
  • SwiftUI Canvasに表示させる
MyNavigationView.swift
struct MyNavigationView: View {
    var body: some View {
        NavigationViewControllerRepresentation()
    }
}

struct MyNavigationView_Previews: PreviewProvider {
    static var previews: some View {
        MyNavigationView()
            .environment(\.colorScheme, .dark) // わかりやすくするためにダークモードで表示
    }
}

2. SafeAreaを調整する

Screenshot 2020-03-14 at 15.11.49.png

この状態だと、赤枠で囲ったように上下に隙間ができます。SafeAreaですね。MyNavigationView.edgesIgnoringSafeArea(.vertical)としたいところですがここではその衝動をぐっと抑えます。

SceneDelegate.swift
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?
    func scene(_ scene: UIScene, willConnectTo _: UISceneSession, options _: UIScene.ConnectionOptions) {

        let contentView = MyNavigationView().edgesIgnoringSafeArea(.vertical) // [1]

        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView: contentView)
            self.window = window
            window.makeKeyAndVisible()
        }
    }

コメントの[1]にあるように、最下層のビューに対してedgesIgnoringSafeArea(.vertical)を適用してあげます。そうするとプレビュー上は隙間がありますが、実機で動かすと確かに上下の隙間がなくなります。

  • 確認結果

右側のスクリーンショット、ステータスバーのところまで暗めの灰色になってるのが見えるでしょうか。

3. TabBarに対応させる

TabBarが絡んでくるとちょっとまた調整が必要になります。試しにそのままTabBarで囲い込んでみます。

MyNavigationView.swift
struct MyNavigationView: View {
    var body: some View {
        /*NavigationViewControllerRepresentation()*/
        TabView {
            NavigationViewControllerRepresentation().tabItem {
                Image(systemName: "1.circle")
                Text("tab1").tag(0)
            }
            Text("tab2").tabItem {
                Image(systemName: "2.circle")
                Text("tab2").tag(1)
            }
        }
    }
}


ちょっと上下にTabBarとNavigationBarが食い込んでますね。これはどういわけか.topで直ります。

SceneDelegate.swift
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?
    func scene(_ scene: UIScene, willConnectTo _: UISceneSession, options _: UIScene.ConnectionOptions) {
        // let contentView = MyNavigationView().edgesIgnoringSafeArea(.vertical)
        let contentView = MyNavigationView().edgesIgnoringSafeArea(.top) 

        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView: contentView)
            self.window = window
            window.makeKeyAndVisible()
        }
    }


...やった、、、のか?と思いましたが右側のスクリーンショットを見てください。ステータスバーのところが暗めの灰色ではなく、黒色になっています。そこで、また調整をします。

MyNavigationView.swift
struct MyNavigationView: View {
    var body: some View {
        /*NavigationViewControllerRepresentation()*/

        TabView {
            NavigationViewControllerRepresentation().tabItem {
                Image(systemName: "1.circle")
                Text("tab1").tag(0)
            }
            .edgesIgnoringSafeArea(.vertical) // 追加
            Text("tab2").tabItem {
                Image(systemName: "2.circle")
                Text("tab2").tag(1)
            }
        }
    }
}

やりましたね。

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