20200114のSwiftに関する記事は10件です。

[Swift] ScrollViewとAutoLayoutを利用してTextFieldやTextViewがキーボードに隠れないようにしよう!

はじめに

プロジェクトを作りことになるとほとんど使うことになるTextField、TextViewの入力パーツ。
入力するためにタップするとキーボードが上がってくるんですが、入力パーツが画面の下の部分にあるとキーボードによって隠れてしましますね。
今回はStoryBoardとAutoLayoutを利用しているプロジェクトでパーツがキーボードに隠れる問題が起きないようにしていきます。

View作成

例として配送情報を入力する画面を作りました。

ScrollView

スクリーンショット 2020-01-14 15.39.46.png
今回はXCode11から追加されたScroll ViewのContent Layout GuidesはOffにします。
そしてScrollViewをControllerいっぱいに設定します。

ScrollViewの中のView

ScrollViewを利用するとき慣れてないと少し難しく感じるかもしれませんがシンプルに考えてScrollViewの中のViewがScrollView以上の大きさを持ってる場合スクロールするようになります。ScrollViewを画面いっぱいに設定したので中のViewが画面より長い場合縦スクロール、横幅が画面の横が幅より大きい場合横にスクロールします。
この原理をキーボードが出てきたとき利用するので覚えておいてください。

スクリーンショット 2020-01-14 15.47.42.png
まず、中のViewをScrollViewいっぱいに設定します。
スクリーンショット 2020-01-14 15.51.52.png
中のViewとcontrolキーを押したままScrollViewの方にドラグ&ドロップしてEqual Widthsを押して幅をScrollViewと同じにします。
スクリーンショット 2020-01-14 15.56.25.png
中のViewの高さは中に配置するTextFieldなどで決めますが、その前までAutoLayoutのエラーが出てるのが気持ち悪いので仮で高さを1000設定します。(後で消します)
ここまででエラーは出なくなってStoryBoard場で縦スクロールが出来るようになっているか確認してみてください!

入力フォーム

配送情報を入力するフォームを作ります。
スクリーンショット 2020-01-14 16.01.41.png
私の場合StackViewを利用しました。
中のフォームは自由に作ってください
スクリーンショット 2020-01-14 16.07.13.png
出来上がった画面はこんな感じです!
スクリーンショット 2020-01-14 16.10.25.png
それでは中のViewのパーツが決まったのでさっき仮で設定しておいた中のViewの高さを消します。
この制約をクリックしてバックスペースを押したら消えます。
スクリーンショット 2020-01-14 16.11.58.png
消した後は中のViewの高さを改めて設定します。私の場合StackViewの一番下の部分に合わせてマージン100を設定しました。
一番下に置いたパーツを利用して制約を決めてください。
これで画面の設定は終わりです!

確認

スクリーンショット 2020-01-14 16.18.11.png
シミュレーターで確認してみるとこんな感じで表示されています。
中のViewがScrollViewより小さい(縦)のでスクロールできない状態ですね。
それでは一番下のパーツを押してみます。
スクリーンショット 2020-01-14 16.19.48.png
やっぱり隠れてしまいましたねー。
これではユーザーは永遠に入力できない状態です。
これからこの問題を解決していきましょう!

Outletを結びつける

これから利用する部分をコードで利用出来るようにします。
必要なパーツはこの二つになります。
1.ScrollView
2.ScrollViewのAutoLayoutのBottom制約
スクリーンショット 2020-01-14 16.28.20.png
Bottom制約はこの部分をドラグ&ドロップするとコードで使えるようになります。

TextFieldのdelegateを利用してキーボードを下げるのでdelegateを利用可能にします。
スクリーンショット 2020-01-14 16.41.07.png
スクリーンショット 2020-01-14 16.42.13.png
それぞれ、TextFieldとcontrolキーを押してViewController側の丸いアイコンにドラグ&ドロップして全てのTextFieldのdelegateをOnにします。

隠れないようにコードを書く

いよいよコードの部分です。

キーボードの動きを感知するためのNotificationを設定とキーボードを下げるためのTextFiledのdelegateを設定

ViewController.swift
import UIKit

class ViewController: UIViewController {

  @IBOutlet weak var scrollView: UIScrollView!
  @IBOutlet weak var scrollViewBottomConstraint: NSLayoutConstraint!

  override func viewDidLoad() {
    super.viewDidLoad()

    setupNotifications()
  }

  private func setupNotifications() {
    //キーボードが表示される時呼ばれるNotification
    NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillChangeFrame), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
    //キーボードが非表示になる時呼ばれるNotification
    NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
  }

  @objc private func keyboardWillChangeFrame(_ notification: Notification) {
    print("キーボード表示")
  }

  @objc private func keyboardWillHide(_ notification: Notification) {
    print("キーボード非表示")
  }
}


//MARK: - UITextFieldDelegate
extension ViewController: UITextFieldDelegate {
  //キーボードReturnキーを押したらキーボードを下げる
  func textFieldShouldReturn(_ textField: UITextField) -> Bool {
    textField.resignFirstResponder()
  }
}

シミュレーターでキーボードが表示される時と非表示になる時ちゃんとprintされているのを確認できました。

キーボードが表示された時のメソッド

ここが今日の目的になります。
スクリーンショット 2020-01-14 17.15.43.png
現在のScrollViewの高さは画面いっぱいの赤い枠になります。
そして、キーボードの高さが青い枠です。
このままだとキーボードの高さ分のScrollViewが隠れるためScrollViewの高さからキーボードの高さを引いてScrollViewの高さを再設定する必要があります。緑の枠が再設定する高さです。
緑の枠がScrollViewの大きさになったらScrollViewのなかのViewがScrollViewより大きくなるのでスクロールが可能になります!
それでは高さを際せってするためにコードをいじってみましょう。

ViewController.swift
@objc private func keyboardWillChangeFrame(_ notification: Notification) {
          //キーボードのサイズ
    guard let keyboardFrame = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue,
          //キーボードのアニメーション時間
          let duration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval,
          //キーボードのアニメーション曲線
          let curve = notification.userInfo?[UIResponder.keyboardAnimationCurveUserInfoKey] as? UInt,
          //Outletで結び付けたScrollViewのBottom制約
          let scrollViewBottomConstraint = self.scrollViewBottomConstraint else { return }

    //キーボードの高さ
    let keyboardHeight = keyboardFrame.height
    //Bottom制約再設定
    scrollViewBottomConstraint.constant = keyboardHeight

    //アニメーションを利用してキーボードが上がるアニメーションと同じ速度でScrollViewのたBottom制約設定を適応
    UIView.animate(withDuration: duration, delay: 0, options: UIView.AnimationOptions(rawValue: curve), animations: {
      self.view.layoutIfNeeded()
    })
  }

スクリーンショット 2020-01-14 17.33.38.png
AutoLayoutを利用しているのでScrollViewのBottom制約をキーボードの高さ分上にグイっと持ち上げるような感じでScrollViewの高さを再設定することができます。
シミュレーターで確認してみてください。
アニメーション付きでいい感じに高さが調整されてキーボードに隠れないようになったと思います????

キーボードが非表示になる時のメソッド

それでは最後にキーボードが下がる時高さが小さくなったScrollViewの高さを画面いっぱいに直してあげる必要があります。

ViewController.swift
@objc private func keyboardWillHide(_ notification: Notification) {
          //キーボードのアニメーション時間
    guard let duration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval,
          //キーボードのアニメーション曲線
          let curve = notification.userInfo?[UIResponder.keyboardAnimationCurveUserInfoKey] as? UInt,
          //Outletで結び付けたScrollViewのBottom制約
          let scrollViewBottomConstraint = self.scrollViewBottomConstraint else { return }

    //画面いっぱいになるのでBottomのマージンを0に戻す
    scrollViewBottomConstraint.constant = 0

    //アニメーションを利用してキーボードが上がるアニメーションと同じ速度でScrollViewのたBottom制約設定を適応
    UIView.animate(withDuration: duration, delay: 0, options: UIView.AnimationOptions(rawValue: curve), animations: {
      self.view.layoutIfNeeded()
    })
  }
}

スクリーンショット 2020-01-14 17.45.02.png
ScrollViewのBottom制約を0に戻してあげるだけで終了です!
シミュレーターで確認するとキーボードが上がる時も上がる時もアニメーションしながらいい感じにScrollViewの高さを調整していますね????
お疲れ様でした!

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

[iOS/Swift] アプリ開発の実務的アプローチで学ぶデザインパターン ~Composite~

この記事シリーズは、iOS/Swiftエンジニアである執筆者個人が、
ごく普通のiOSアプリ開発でよくある状況
Swiftのコアライブラリやフレームワークで使われているパターン
着目してデザインパターンを学び直してみた記録です。

関連記事一覧
[iOS/Swift] アプリ開発の実務的アプローチで学ぶデザインパターン

Compositeパターン概要

  • ディレクトリとファイルのような、ツリー構造を扱うためのパターンです。
  • ディレクトリとファイルのように、「名前」などの同じプロパティや、「削除」などの同じ操作を持つ場合、ディレクトリ(容器)とファイル(中身)を同じように扱うことができます。
  • すなわち、検索や削除などを再帰的に行うことができます。
  • GoFのデザインパターンでは構造に関するパターンに分類されます。

使い所

  • そのものズバリ、ツリー構造を扱う場合には、Compositeパターンを思い浮かべると設計に掛かる時間を短縮できる可能性が高いです。
  • UIViewのView Hierarchyもツリー構造であり、Compositeパターンが使われています。

view_hierarchy.gif
引用:Cocoa Design Patterns (Retired Document)

サンプルコード

Swiftバージョンは 5.1 です。

protocol DirectoryEntry {
    var name: String { get }
    func remove()
}

final class File: DirectoryEntry {
    let name: String

    init(name: String) {
        self.name = name
    }

    func remove() {
        print("\(name)を削除しました")
    }
}

final class Directory: DirectoryEntry {
    let name: String
    private var entryList = [DirectoryEntry]()

    init(name: String) {
        self.name = name
    }

    func add(entry: DirectoryEntry) {
        entryList.append(entry)
    }

    func remove() {
        for entry in entryList {
            entry.remove()
        }
        print("\(name)を削除しました")
    }
}

// Usage
let dir1 = Directory(name: "dir1")
let file1 = File(name: "file1")
dir1.add(entry: file1)
// dir1
//   ∟file1

let dir2 = Directory(name: "dir2")
let file2 = File(name: "file2")
let file3 = File(name: "file3")
dir2.add(entry: file2)
dir2.add(entry: file3)
// dir2
//   ∟file2
//   ∟file3

dir1.add(entry: dir2)
// dir1
//   ∟file1
//   ∟dir2
//     ∟file2
//     ∟file3

let file4 = File(name: "file4")
dir1.add(entry: file4)
// dir1
//   ∟file1
//   ∟dir2
//     ∟file2
//     ∟file3
//   ∟file4

dir1.remove()
// "file1を削除しました"
// "file2を削除しました"
// "file3を削除しました"
// "dir2を削除しました"
// "file4を削除しました"
// "dir1を削除しました"
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[iOS/Swift] アプリ開発の実務的アプローチで学ぶデザインパターン ~Command~

この記事シリーズは、iOS/Swiftエンジニアである執筆者個人が、
ごく普通のiOSアプリ開発でよくある状況
Swiftのコアライブラリやフレームワークで使われているパターン
着目してデザインパターンを学び直してみた記録です。

関連記事一覧
[iOS/Swift] アプリ開発の実務的アプローチで学ぶデザインパターン

Commandパターン概要

  • Command=命令をオブジェクトとして扱うパターンです。
  • Commandオブジェクトは、「パラメータ」と「処理」をカプセル化したモノ。
  • GoFのデザインパターンでは振る舞いに関するパターンに分類されます。

使い所

  1. 並列処理を行いたいケース
  2. Queueing処理(Commandの待ち行列管理)を行いたいケース
  3. (DBを使わずに)ある時点の状態へのロールバックを行いたいケース
  4. Commandオブジェクトの生成元とは異なる場所でCommandを実行したいケース
  • 上記1、2を実現するための言語機能であるOperation(とOperationQueue)は、Commandパターンに則って構築されています。
  • したがってSwiftでは、Commandパターンのコードを自力で実装しなければならないケースは少なそうです。
  • とはいえ前提知識として、Commandパターンのコンセプトは理解しておいて損はないかと思います。

(ご参考)
【Swift】Grand Central Dispatch (GCD)とOperationQueue まとめ > Operation Queue

サンプルコード(Commandパターンの実装例)

Swiftバージョンは 5.1 です。

class Command {
    let receiver: Receiver

    init(receiver: Receiver) {
        self.receiver = receiver
    }

    func execute() {}
}

class HelloCommand: Command {
    override func execute() {
        receiver.action(message: "Hello")
    }
}

class WorldCommand: Command {
    override func execute() {
        receiver.action(message: "World")
    }
}

protocol Receiver {
    func action(message: String)
}

class PrintReceiver: Receiver {
    func action(message: String) {
        print(message)
    }
}

class Invoker {
    private var commands = [Command]()

    func addCommand(_ command: Command) {
        self.commands.append(command)
    }

    func execute() {
        for command in commands {
            command.execute()
        }
    }
}

let invoker = Invoker()
let receiver = PrintReceiver()
let hello = HelloCommand(receiver: receiver)
let world = WorldCommand(receiver: receiver)
invoker.addCommand(hello)
invoker.addCommand(world)

invoker.execute()

// 処理結果
// "Hello"
// "World"

引用元:
しめ鯖日記 - Swiftで学ぶデザインパターン22 (Commandパターン)

サンプルコード(Operationの実装例)

import Foundation
class MyCommand: Operation {
    var commandName: String
    var delay: Int

    init(commandName: String, delay: Int) {
        self.commandName = commandName
        self.delay = delay
    }

    override func main() {
        for _ in 0..<delay {
            sleep(1)
        }
        print(commandName)
    }
}

let operationQueue = OperationQueue()
// 最大同時実行数
operationQueue.maxConcurrentOperationCount = 3


let command1 = MyCommand(commandName: "command1", delay: 1)
let command2 = MyCommand(commandName: "command2", delay: 3)
let command3 = MyCommand(commandName: "command3", delay: 2)

operationQueue.addOperation(command1)
operationQueue.addOperation(command2)
operationQueue.addOperation(command3)

// 処理結果
// "command1"
// "command3"
// "command2"

OperationとOperationQueueを使ってみると、Commandパターンになっていることが理解できるかと思います。

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

[iOS/Swift] アプリ開発の実務的アプローチで学ぶデザインパターン ~Chain Of Responsibility~

この記事シリーズは、iOS/Swiftエンジニアである執筆者個人が、
ごく普通のiOSアプリ開発でよくある状況
Swiftのコアライブラリやフレームワークで使われているパターン
着目してデザインパターンを学び直してみた記録です。

関連記事一覧
[iOS/Swift] アプリ開発の実務的アプローチで学ぶデザインパターン

Chain Of Responsibilityパターン概要

  • 全体を統率するクラスを作らず、個々のクラスが責務を「たらい回し」して、責務を果たせるクラスに行き当たった時点でそのクラスが処理を行います。
  • GoFのデザインパターンでは振る舞いに関するパターンに分類されます。

使い所

  • iOSエンジニアにとってはおなじみの、UIKitの「Responder Chain」という仕組みがChain Of Responsibilityパターンに則って構築されています。

Responder Chainとは?

UIResponderオブジェクトの繋がりのことで、この繋がりの順番にしたがって
touchイベントなどの処理ができるUIResponderオブジェクト(=first responder)を見つけて処理を行う仕組みです。

引用:【Swift】Responder Chainの仕組み

  • アプリ開発での使い所は思い当たらなかったのですが、iOSフレークワーク内部で使われている仕組みであるわけですから、概念を理解しておいて損はないかと思います。

サンプルコード

Swiftバージョンは 5.1 です。

protocol Withdrawing {
    // 引き出す
    func withdraw(amount: Int) -> Bool
}

// 札束クラス
final class MoneyPile: Withdrawing {
    // 額面
    let value: Int
    // 枚数
    var quantity: Int
    // 責務をたらい渡す次のオブジェクト
    var next: Withdrawing?

    init(value: Int, quantity: Int, next: Withdrawing?) {
        self.value = value
        self.quantity = quantity
        self.next = next
    }

    func withdraw(amount: Int) -> Bool {

        var amount = amount

        func canTakeSomeBill(want: Int) -> Bool {
            return (want / self.value) > 0
        }

        var quantity = self.quantity

        while canTakeSomeBill(want: amount) {

            if quantity == 0 {
                break
            }

            amount -= self.value
            quantity -= 1
        }

        guard amount > 0 else {
            return true
        }

        if let next = self.next {
            return next.withdraw(amount: amount)
        }

        return false
    }
}

// ATMクラス
final class ATM: Withdrawing {
    // $100の札束
    private var hundred: Withdrawing
    // $50の札束
    private var fifty: Withdrawing
    // $20の札束
    private var twenty: Withdrawing
    // $10の札束
    private var ten: Withdrawing

    private var startPile: Withdrawing {
        return self.hundred
    }

    init(hundred: Withdrawing,
           fifty: Withdrawing,
          twenty: Withdrawing,
             ten: Withdrawing) {

        self.hundred = hundred
        self.fifty = fifty
        self.twenty = twenty
        self.ten = ten
    }

    func withdraw(amount: Int) -> Bool {
        return startPile.withdraw(amount: amount)
    }
}

// Usage
// 札束オブジェクトを生成して $10 -> $20 -> $50 -> $100 と数珠つなぎにする
let ten = MoneyPile(value: 10, quantity: 6, next: nil)
let twenty = MoneyPile(value: 20, quantity: 2, next: ten)
let fifty = MoneyPile(value: 50, quantity: 2, next: twenty)
let hundred = MoneyPile(value: 100, quantity: 1, next: fifty)

// ATMオブジェクトを札束オブジェクトを格納して生成する
var atm = ATM(hundred: hundred, fifty: fifty, twenty: twenty, ten: ten)

// ATMには合計$300しか入っていないため$310は引き出せない
atm.withdraw(amount: 310)   // false

// $300は引き出せる
atm.withdraw(amount: 100)   // true

引用:
https://github.com/ochococo/Design-Patterns-In-Swift#-chain-of-responsibility

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

[iOS/Swift] アプリ開発の実務的アプローチで学ぶデザインパターン ~Bridge~

この記事シリーズは、iOS/Swiftエンジニアである執筆者個人が、
ごく普通のiOSアプリ開発でよくある状況
Swiftのコアライブラリやフレームワークで使われているパターン
着目してデザインパターンを学び直してみた記録です。

関連記事一覧
[iOS/Swift] アプリ開発の実務的アプローチで学ぶデザインパターン

Bridgeパターン概要

  • 機能を提供する層と、実装を提供する層を分離するパターンです。
  • 各層の橋渡し(Bridge)をiOSエンジニアにはおなじみの委譲(delegate)で行います。
  • GoFのデザインパターンでは構造に関するパターンに分類されます。

使い所

  • 実装をMockに差し替えることができ、ユニットテストがしやすくなります。
  • VIPERなどのアーキテクチャーパターンは、Bridgeパターンによって成立しています(と私は認識しています)。

サンプルコード

Swiftバージョンは 5.1 です。

// VIPERでのPresenterに当たる層
protocol Presentation {
    var interactor: Usecase { get set }
    func searchButtonTapped()
}

final class Presenter: Presentation {
    var interactor: Usecase

    func searchButtonTapped() {
        self.interactor.fetch()
    }

    init(interactor: Usecase) {
        self.interactor = interactor
    }
}

// VIPERでのInteractorに当たる層
protocol Usecase {
    func fetch()
}

final class ProductionUsecase: Usecase {
    func fetch() {
        print("fetch for Production")
    }
}

final class MockUsecase: Usecase {
    func fetch() {
        print("fetch for Mock")
    }
}

// VIPERでのViewに当たる層
let production = Presenter(interactor: ProductionUsecase())
production.searchButtonTapped()    // "fetch for Production"

let test = Presenter(interactor: MockUsecase())
test.searchButtonTapped()    // "fetch for Mock"
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SwiftにおけるJSONとCodableの変換方法(エンコード)

こんにちは、ふみっちです。この記事を読んでくださっているみなさん早々ですが、JSONをCodableでパースするときはどのように行っていますか?

僕は大体、JSONをCodableに変換する際には脳死で

struct Model: Codable {
    let aaa: String
    let bbb: String
}

var json = [
    "aaa": "あああ",
    "bbb": "ビビビ"
]

guard let data = try? JSONSerialization.data(withJSONObject: json, options: []) else {
    print(json)
    fatalError()
}

let model = try? JSONDecoder().decode(Model.self, from: data)

このようなコードを書いていました。上のコードはうまくいきます。でも、

struct Model: Codable {
    let aaa: String
    let bbb: String
}

// ここだけ変更
var json = """
{
"aaa": "あああ",
"bbb": "ビビビ"
}
"""

guard let data = try? JSONSerialization.data(withJSONObject: json, options: []) else {
    print(json)
    fatalError()
}

let model = try? JSONDecoder().decode(Model.self, from: data)

JSONを String として扱うと、JSONSerialization.dataでうまくエンコードしてくれません。

解決策は、String().dataを使用すれば解決します。

struct Model: Codable {
    let aaa: String
    let bbb: String
}

var json = """
{
"aaa": "あああ",
"bbb": "ビビビ"
}
"""

// String().data
let data = json.data(using: .utf8)!

let model = try! JSONDecoder().decode(Model.self, from: data)

初歩的な内容ですが、Google先生と一緒に記事を探してもここら辺の区別をしているSwiftにおけるエンコード・デコードの記事が日本語でなかなか見つからなかったのでメモとして書かせていただきました。

短いですが最後にこのようなミスを犯さないために、
JSONObject...Dictionary(他にもあるかも。よければコメントください。)
JSON ... String
っていう風に考えておくと、間違えることが減ると思うので参考にしてみてください。

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

[iOS/Swift] アプリ開発の実務的アプローチで学ぶデザインパターン ~Facade~

この記事シリーズは、iOS/Swiftエンジニアである執筆者個人が、
ごく普通のiOSアプリ開発でよくある状況
Swiftのコアライブラリやフレームワークで使われているパターン
着目してデザインパターンを学び直してみた記録です。

関連記事一覧
[iOS/Swift] アプリ開発の実務的アプローチで学ぶデザインパターン

Facadeパターン概要

  • 煩雑なモノの窓口(=Facade、ファサード)を作ることでシンプルに扱えるようにするパターンです。
  • Facadeクラスは複雑な実装は持たず、(大抵は複数の)モジュールに対する問合せ窓口としての機能だけを提供します。
  • GoFのデザインパターンでは構造に関するパターンに分類されます。

使い所

アプリ開発の実務で適用できるケースはたくさんあると思いますが、例えば

  • UserDefaultsの読み込み・書き込み とか
  • KeyChainの読み込み・書き込み とか

サンプルコード

Swiftバージョンは 5.1 です。

// Example
final class Defaults {

    private let defaults: UserDefaults

    init(defaults: UserDefaults = .standard) {
        self.defaults = defaults
    }

    subscript(key: String) -> String? {
        get {
            return defaults.string(forKey: key)
        }

        set {
            defaults.set(newValue, forKey: key)
        }
    }
}

// Usage
let storage = Defaults()

// Store
storage["Bishop"] = "Disconnect me. I’d rather be nothing"

// Read
storage["Bishop"]

引用:
https://github.com/ochococo/Design-Patterns-In-Swift#-fa%C3%A7ade

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

初の個人アプリを App Store に審査出したら 30 分足らずでリリースされてビビったからその全てをお伝えします

明けましておめでとうございます。新しい年ということでアイコンも一新した lovee です。はい、あまりにもビビったのでラノベっぽいタイトルにしてみました。後悔はしていない。

なぜアプリ作ろうと思ったの?

いやーそれなりに長い間 iOS エンジニアやってきたけど、業務ではそれなりにたくさんのアプリを作ってきたけど、完全に個人でアプリを作ったことがまだ全くないんですよね。というわけで去年「今年中に個人アプリを出す」という目標を立てました。結局去年中には出せなかったがとりあえずまだ今年度中ということで開き直ってます

どんなアプリ?

簡単に言うとただ単にユーザが入力した文字列を QR コードに変換するだけのアプリです。それだけです。でも本当の使い方はアプリをいちいち立ち上げて QR コードを作るのではありません。このアプリは iOS ネイティブの共有メニューを対応しているので、別のアプリから共有メニュー呼び出して共有したいものを QR コードに変換するのが本来の使い方というかアイデアです。

言葉では説明しにくいので、こちらの画像をご覧いただければ分かりやすいかもしれません:

quickshare.gif

はい、こちらは bilibili 動画で周りにいる人に布教したい動画があったときに、サクッとその動画の URL を QR コード化した動画です。これで例えば周りにいる人が AirDrop が使えなかったり、その人の SNS アカウントを探すのが面倒だったり、そもそも繋がってなくてたまたま懇親会とかで初対面の人に何かを布教したいときにこのサクッと作られた QR コードを読み取ってもらえばミッションコンプリートです。とても楽です。もちろん bilibili 動画だけでなく、iOS のネイティブ共有メニューが使えるアプリで共有内容がテキストもしくは URL に落とし込めるものならなんでも使えます。もう一つの例としては周りにいる友達と何処かを目指したいとき、その場所も地図アプリから共有できたりします:

quickshare_map.gif

App Store で無料リリースしており、しかも広告もないので是非試してみてください!

https://apps.apple.com/app/quickshare-share-via-qr-code/id1494461342

ちなみに実はソースコードも公開しており、興味ある方是非覗いてみてください:

https://github.com/el-hoshino/QuickshaRe

どんな技術を使ってる?

QR コード生成

QR コードの生成自体は特に尖った技術はありません、単純に CoreImage の CIFilter.qrCodeGenerator() から作られてるだけです。ただ何か特筆したいことがあるとすれば、その生成コード読めばわかるように、今の CoreImage の Swift 対応がだいぶ様になっているようです。ひと昔はまだ CIFilter(name: String) から作らなくてはならず、そのため作られたインスタンスはただの CIFilter 型なので、それはなんのためのフィルターか、どんなパラメーターが設定できるかなどはドキュメントを読まなくてはならず、全て文字列で Key-Value コーディングする必要がありました。ところが今の作り方では CIFilter & CIQRCodeGenerator 型が作られ、CIFilter はこれまでと同じく、そして CIQRCodeGenerator プロトコルは設定できるパラメーターとかが定義されているので、QR コードで設定できる correctionLevelmessage がプロパティーとして直接設定できるようになり非常に使いやすいです。

QRCodeGenerator
private extension CIQRCodeGenerator where Self: CIFilter {

    func qrCodeImage(for text: String, correctionLevel: String) -> CIImage? {

        self.correctionLevel = correctionLevel
        self.message = text.data(using: .utf8)!

        return outputImage

    }

}

QR コード表示

CIFilter で生成された QR コードはサイズが非常に小さく、各ビットは 1 ピクセルで表現されているため、そのまま画面に出すととても小さいです。どれくらい小さいかというとこんな感じの画像になります:

Simulator Screen Shot - iPhone 8 - 2020-01-13 at 18.24.22のコピー.png

もちろん確かにこの画像では QR コードとして必要なすべての情報が詰まっていますが、小さすぎてカメラからは認識できませんので画面に合わせて大きく表示する必要があります。ちなみにメインアプリでは SwiftUI を使っています。さて次の問題はどうやって小さい画像を SwiftUI で大きく表示させるかの問題です。

まず画面に合わせて大きく表示すること自体は簡単です、Image 要素の後ろに .resizable().scaledToFit() を追加してあげればいいです。前の .resizable()Image 要素を親要素に合わせてリサイズし、後ろの .scaledToFit() はリサイズしたときアスペクト比を保持して親要素にフィットさせるようにします。UIKit 時代の .aspectFit ですね。

しかしこれだけでは大きな問題点が残っています、それは SwiftUI では(UIKit も同じですが)画像をリサイズするとき、特に拡大するときに補間が働いて、画像がぼやけてしまい、こんなふうに表示されます:

Simulator Screen Shot - iPhone 8 - 2020-01-13 at 18.38.29.png

もちろんこれでスキャンするときちゃんと認識はできますが、やはり見た目は非常に残念ですね。ではこれをどうすればいいかというと、Image 要素を .interpolation(.none) で補完を無効化すればいいです。ですので最終的なコードはこんなものになります:

QRCodeImageView
            Image(uiImage: generator.qrPicture(for: content).uiImage)
                .interpolation(.none)
                .resizable()
                .scaledToFit()

こうすれば下記のようにキレイに QR コードが表示されます:

Simulator Screen Shot - iPhone 8 - 2020-01-13 at 18.47.54.png

SwiftUI

せっかく SwiftUI の話が出てきたので、ここでもう一歩踏み入れた話をしましょう。なぜかというととにかく SwiftUI は今現時点ではすごい罠だらけですので()

まずは入力画面の話をしましょう。入力画面はこんな感じのとてもシンプルな画面です。

Simulator Screen Shot - iPhone 11 Pro Max - 2020-01-13 at 21.29.23.png

とても簡単ですね、画面には左側に文字入力の TextField、そしてその右側に入力確定の Button しかありません。少なくともそんな風に見えます。…本当にそうでしょうか?

まず TextField について、今回はしたに線を引いてあります。UIKit の時代、まあ下線を引くのは決して簡単とは言えませんが、まあ愚直な形でできてました。しかし現段階の SwiftUI では、四角のボーダーや背景色を入れるなら楽に対応できますが下に線を一本引くだけの描画は直接にはできません。そこでいろいろ調べた結果、どうやら Divider() を下に入れればそれっぽく見えることがわかりましたので、こんな感じで対応しました:

TextInputView
extension View {

    func underline() -> some View {
        return VStack {
            self
            Divider()
        }
    }

}
TextInputView
    TextField(/* ... */)
        .underline()

次は右側のボタンですが、まずこのボタンの動作は Push 遷移になるので、そもそも Button ではなく NavigationLink になります。まあこれはそんなに難しい話ではないですかね。

問題はこのボタンの周りにあるボーダーです。

え?さっきボーダー入れるなら楽って言ってたよね?と思うかもしれません。実際筆者もこの対応をするまで楽だと思ってました。そもそも昔 beta の時代は確かに楽に対応できてた気はします。確か .cornerRadius(10).border(Color.secondary, width: 1) みたいなこと書いてればできた覚えがあります。ところが今現時点(Xcode 11.3)でやってみたら、なんとボーダーが直角のままでした。.cornerRadius.border の順番入れ替えても直角のボーダーのままで角の線がなくなっただけです。

というわけでいろいろ調べてみたら、どうやら .overlay で設定しなくてはいけないことが判明しましたので、こんな風に作ることになりました:

TextInputView
extension View {

    func border(color: Color, cornerRadius: CGFloat, lineWidth: CGFloat) -> some View {

        return overlay(
            RoundedRectangle(cornerRadius: cornerRadius)
            .stroke(color, lineWidth: lineWidth)
        )

    }

}
TextInputView
    NavigationLink(/* ... */)
        .border(color: .secondary, cornerRadius: 10, lineWidth: 1)

面倒ですね。

面倒と言えば、さらにこのままこれを NavigationView の中に表示すると、機種によってはキーボードを呼び出したとき TextField が隠されちゃうので、さらにこれらを .offset(y: -100) で場所をずらしました。面倒です。よしなりにレイアウトやってくれるならキーボード表示時の対応くらい自動的によしなりにやって欲しいです。でないとちゃんとしたレイアウトを作りたいとき結局 GeometryReader とか使う羽目になるからとても面倒です。

面倒な話はまだここで終わっていません。Push 遷移の話をちょろっとしましたが、さてこの Push 遷移をするためには、この入力ビューを NavigationView の中に入れなくてはいけません。というわけで SceneDelegate を対応させましょう:

SceneDelegate
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        let contentView = NavigationView(content: { TextInputView() })
        // ...
    }

これで手持ちの iPhone X で動作確認してみましょう。よっしちゃんと表示されました。文字列入力して Generate ボタンタップしたらちゃんと QR コードも表示されました。

Simulator Screen Shot - iPhone 11 Pro Max - 2020-01-14 at 02.23.32.png

めでたしめでたし。これで App Store に申請出しましょう。

と思うじゃん?また後でテストについてもちょっと話しますが、実はこれ、iPad ではちゃんと表示されません(!?)。アプリ開いたら真っ白な画面が表示されます。

Simulator Screen Shot - iPad Pro (12.9-inch) (3rd generation) - 2020-01-13 at 23.41.32.png

なんでや!?と思っていろいろ画面いじってみたら、どうやら指が左から右スワイプしないとこの入力画面が出てこないことが判明しました。

Simulator Screen Shot - iPad Pro (12.9-inch) (3rd generation) - 2020-01-13 at 23.42.24.png

さらにいうと iPad だけでなく、iPhone でも横画面にするとき横の Size Class が Regular になる iPhone 11 Pro Max とかの機種も同じく真っ白で左から右スワイプしないといけません。

Simulator Screen Shot - iPhone 11 Pro Max - 2020-01-13 at 23.43.39.png

というわけでこの動きについてさらにいろいろ調べてみたら、とりあえず DoubleColumnNavigationViewStyle にすれば表示はできることがわかりました。ただしこれは本来すべき対応というより、現状のバグのような動きに対するワークアラウンドですから本当はこれに依存すべきではないですが、まあトレードオフですかね、多機種/多画面向きの対応を取るのか、SwiftUI がバグらない環境を取るのか。とりあえず私は前者を取りました。そしてもちろんこうすれば QR コードの表示画面は iPad ではフル画面ではなく常に右側の限られた幅で表示されることにはなっちゃいますが。というわけでコードをこんなふうに修正しました:

SceneDelegate
        let contentView = NavigationView(content: { TextInputView() })
            .navigationViewStyle(DoubleColumnNavigationViewStyle())
            .padding()        

Simulator Screen Shot - iPad Pro (12.9-inch) (3rd generation) - 2020-01-13 at 23.44.55.png

しかし、これで万事解決と思ったら、まだまだ罠があります。先ほど言いました iPhone 11 Pro Max の横画面問題ですが、こうしても横画面では結局これまでと同じく右スワイプしないと入力画面出てきません。もう現在これについては SwiftUI のバグということで開き直って放置することにしました。別にアプリが落ちるわけでもないし

こんなにシンプルな画面が 2 つだけでも対応がこんなに大変なので、現時点で SwiftUI で複雑な画面を組むのはやはりやめたほうがいいですね。Swift が 3.0 でようやくまともっぽくなったのと同じように、SwiftUI 3.0 くらいまで気長に待ちましょう。

Share Extension 対応

共有メニューに自分のアプリを表示させるためには Share Extension を対応すればいいです。まずは Target を追加します:

スクリーンショット 2020-01-13 18.53.02.png

あとはダイアログに合わせてポチポチすれば Share Extension 用の Target が追加されます。簡単です。

ところがこれで共有画面用の ShareViewController が自動的に追加されますが、その VC のクラスは Twitter とかの SNS に投稿するための iOS ネイティブで用意されている SLComposeServiceViewController のサブクラスです;しかしこのアプリではそんな SNS 投稿とかをせず、単純に共有内容を QR コード化して表示するだけの画面が欲しいです。どうすればいいですか?

答えは意外と簡単です:SLComposeServiceViewController の継承を UIViewController に変えるだけです。

ShareViewController
- class ShareViewController: SLComposeServiceViewController {
+ class ShareViewController: UIViewController {

    // ...

}

こうすることで、簡単に必要な共有画面を組み込めます。

また、共有項目の取得は、ShareViewControllerextensionContext?.inputItems から取得できます。ただしこの inputItems[Any] ですので、中身を NSExtensionItem に落とし込む必要があります。落とし込んだら、さらに attachments プロパティーから forEach して loadItem の非同期処理で共有アイテムを取り出さなくてはなりませんし、そのアイテムの種類も Uniform Type Identifiers で特定しないといけなかったりします。割と結構面倒な処理ですが、愚直なコードにするとこんな感じです:

ShareViewController
class ShareViewController: UIViewController {

    private typealias SharingItem = (type: String, content: String)
    private var sharingItems: [SharingItem] = []

    override func viewDidLoad() {
        super.viewDidLoad()

        guard let inputItem = extensionContext?.inputItems.first as? NSExtensionItem else {
            return
        }

        DispatchQueue.global().async {

            for attatchment in inputItem.attachments ?? [] {
                for identifier in attatchment.registeredTypeIdentifiers {

                    attachment.loadItem(forTypeIdentifier: identifier) { [weak self] (coding, error) in

                        guard let item = coding else {
                            assertionFailure("Failed to load item for \(identifier). Error: \(error as Any)")
                            return
                        }

                        switch identifier as CFString {
                        case kUTTypeURL:
                            guard let url = item as? URL else { assertionFailure("Failed to load item as URL."); break }
                            self?.sharingItems.append(("URL", url.absoluteString))

                        case kUTTypeText, kUTTypePlainText:
                            guard let text = item as? String else { assertionFailure("Failed to load item as Text."); break }
                            self?.sharingItems.append(("Text", text))

                        default:
                            break
                        }

                    }

                }

            }

        }

    }

}

見ての通り、非常に長い処理をしなくてはいけないです。ですので流石にアプリではいくつかの細かいメソッドに切り分けていますが。

簡単にちょっと説明を付け加えると、上記の attachments 配列に対してこんなことを行っています:

  1. attachments を回して、さらに各々の attachment に対してそれぞれに登録されている registeredTypeIdentifiers 配列を回します。二重ループです。
  2. registeredTypeIdentifiers ループ内で取得された identifier を使って、attachmentloadItem(forIdentifier:completionHandler:) 処理を掛けます。登録されてるはずの identifier ですので、completionHandler 内では coding が必ず取得できるはずですから、取得できなかった時をアサートします。当然ですが completionHandler は非同期処理ですので注意しましょう。
  3. identifier は Uniform Type Identifiers(UTType) ですので、ここを参考に、URL とテキストを指す kUTTypeURLkUTTypeTextkUTTypePlainText のときだけ共有アイテムを取り出して、自身の sharingItems に追加します。

まあなかなか大変な作業ですね。特に気をつけないといけないのは、kUTTypeURL とかを定義しているのは MobileCoreServices モジュールですので、これも import しないといけないですし、これらはそもそも String ではなく CFString ですので,switch 文回す時もキャストが必要です。さらに、kUTTypeURL の時と、kUTTypeText kUTTypePlainText の時、取り出した coding は違う型で、前者は URL、後者は String になるのでここでもさらにキャストしないといけないです。まあ面倒です。

ちなみにここももちろん生成された小さい QR コード画像を拡大して表示しないといけないですが、これは SwiftUI ではなく UIKit の画面になるので対応が少々違います、UIImageViewlayer を弄ることになります:

ShareViewController
    qrImageView.layer.magnificationFilter = .nearest

さてここまで弄ってミッションコンプリート思ってるあなた、甘いです。まだ画面を閉じる処理が書かれていません。

共有画面を閉じるなんて、画面の上からしたスワイプすれば閉じれるんじゃないの?と思うかもしれませんが、大体の場合はこれでいけます。ところが筆者がいろんなアプリで動作テストしてたときに、なんとなぜか Amazon アプリからの共有画面は閉じれないことが判明しました。ほとんどのアプリでは Modal 遷移するときにカードのような表示になりますが、なぜか Amazon のアプリだけ Fullscreen の表示になります。そのため下スワイプでの画面終了はできないのです。

いやできなくても普通に dismiss すればいいじゃん?と思うかもしれませんが、実際筆者も最初そう思いましたが、やってみたら全然閉じれませんでした。いろいろ調べてみたらなんとまた VC の extensionContext から completeRequest もしくは cancelRequest を呼び出さなくてはいけないことが判明しました。まあ QR コードの生成は終わってるし他にやることもないので complete でいいと思いましたので、こんなふうに素直に閉じるボタンを作って,閉じる処理を書きました:

ShareViewController
    @IBAction private func dismiss() {
        extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
    }

どうやら Share Extension 対応の肝は extensionContext っぽいですね。

さて、ここまで対応したらプログラム上の対応はほぼ終わりましたが、審査に提出するためにはまだもう一つ直すところがあります:Info.plist です。

Share Extension 対応を追加するときに、Xcode は自動的に Share Extension ターゲットの Info.plist に NSExtensionActivationRule を書き込みますが、初期値は開発がしやすいように TRUEPREDICATE になっています。これはどういう意味かというと、アプリから共有メニューを呼び出すときに、共有内容がどんなものでもこのアプリは受け付けられるよという意味です。しかしこれは本当に初期開発がしやすいためだけのものですので、このまま審査に提出するとリジェクトを喰らいます。ですのでこれを直す必要があります。

単純な直し方でしたら、この NSExtensionActivationRule の値タイプを String から Dictionary に変えて、その下にさらに NSExtensionActivationSupportsAttachmentsWithMaxCountNSExtensionActivationSupportsWebURLWithMaxCount などの項目を追加すればいいです。このアプリも受け付ける共有は文字列と URL だけですので、それっぽく設定してみました。ところがいろんなアプリで試してみたところ、TRUEPREDICATE なら対応できてたマップアプリの場所共有はどう変更しても出てこなくなりました。受付タイプを文字列と URL に限定せずに全てのファイルを受け付けるとかにしてもダメでした。というわけでここで更にいろいろ探してみたら、どうやらこれは Subquery で対応できるらしい。というわけで最終的に NSExtensionActivationRule はこんな風になりました:

Info.plist
            <key>NSExtensionActivationRule</key>
            <string>SUBQUERY(extensionItems, $extensionItem, SUBQUERY($extensionItem.attachments, $attachment, ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.text").@count &gt;= 1).@count &gt;= 1
                 OR SUBQUERY(extensionItems, $extensionItem, SUBQUERY($extensionItem.attachments, $attachment, ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.plain-text").@count &gt;= 1).@count &gt;= 1
                 OR SUBQUERY(extensionItems, $extensionItem, SUBQUERY($extensionItem.attachments, $attachment, SUBQUERY($attachment.registeredTypeIdentifiers, $uti, $uti UTI-CONFORMS-TO "public.url" AND NOT $uti UTI-CONFORMS-TO "public.file-url").@count &gt;= 1).@count &gt;= 1).@count &gt;= 1
            </string>

本当はソースコードと同じように kUTTypeURL とかを使いたかったのですが、やはり Info.plist ではそれが使えませんでしたから文字列をそのまま書くしかないです。残念。

初めてリリースした個人アプリどんな感想?

とりあえずまずは審査の速さにビビっていますね。はい。

思い返せば、自分が初めて iPhone アプリ(当時はまだ iPad がない)開発に(一応)携わった時はまだ iPhone 3GS の時代で、当時は Retina ディスプレイとか多種多様な画面サイズとかとは無縁な世界でした。その時にメモオフの iPhone 移植の仕事に携わって、当時は審査を出してから結果が出るのに 1 週間かかってもおかしくない時代でした(コンシューマゲーム機の場合、ソニーやニンテンドーなどの審査は更に時間かかるので特にそれほど違和感がなかったっぽい)。その後インディーズの iOS 開発者が増え、(特に Android の無審査と比べた場合の)審査の待ち時間の不満が高まっていき、徐々に審査が早くなり、1 週間以内、3 ~ 4 日内、2 日以内、そして割と最近でも大体当日か翌日とかのイメージでしたので、まさか 30 分以内に審査が通って App Store に並ばれたなんて思いもしなかったから、とても感慨深いです。

スクリーンショット 2020-01-12 13.16.51.png

そしてまだ超シンプルな機能しかありませんが、むしろ「とりあえずリリースしようぜ」の精神でリリースしたわけです。でないとまじでずるずるして今年度すら出せないじゃないかと危機感を抱きました

Ck9_MOJWYAAR_Rm.jpeg

ところが去年の WWDC の二大目玉の一つである SwiftUI ですが、最初はざわざわして、みんな大興奮して、そして時とともになんとなくみんな冷静になり始めて、「あれ?これ罠多くね?」って流れになって、最終的に「やはりプロダクトに使うのはまだ早すぎた」という結論に着地したのが身をもって理解しました。本当にまだ罠が多いしちょっとでもカスタムなレイアウトを組もうとするとすぐ面倒な書き方が強いられます。特に iPhone と iPad 両方、画面向きも横縦両方対応しようとすると本当に面倒です。まあそれでも個人的には Auto Layout よりは将来性があると思う信じ込んでいますけどね。

将来性といえば、そういえば beta の時 SwiftUI のプレビューをポチポチするだけでビューの添削とかカスタマイズとかができたはずですが、いつの間になくなった!?

そして自分でアプリをリリースして気づいたのが、AppStoreConnect のアプリ紹介でスクショやキーワードの追加や修正は新しいバージョンを出さないとできないですね。静止画のスクショしかないとやはりメリットが伝わりにくいと思って、この記事の最初に貼り付けたような共有メニューからの利用を画面録画したから、それを AppStoreConnect に追加しようと思ったらできませんでした。一つ勉強になりました。

どれくらいダウンロードされた?

残念ながら本記事を執筆した時点ではまだダウンロード数とかのレポートがないんですよね…一応おかげさまで評価が 1 件星 5 ついただいておりますがレビューも今のところまだありません。今後に期待です。皆さんのレビューもお待ちしております。

今後のロードマップは?

テストをたくさん書く

今現時点でも一応最低限の Unit Test がありますが、それに加えて UI Test もたくさん追加していきたいと思います。実際現状まだリリースしていない開発ブランチに最初の UI Test を追加しました。やはりテスト大事ですね。実は上にも書いた iPad の NavigationView 問題、これもとりあえず iPad で UI Test 動かした時に発覚した問題です。

UI Test 書いてて思ったのは、やはり結構 API が独特というか普段の開発とは全然違うスタイルですね。手元の iOS テスト全書がとても頼りになりましたが、それでも今 iPhone と iPad でそれぞれ微妙に違いが生じる操作法をどうやって UI Test に落としこむかちょっと悩んでいます。そしてそれとは別にもしかすると XCUITest 用のラッパーフレームワークを作るかもしれません。いろんな API をもう少し分かりやすいようにしたいです。

そしてまだメインアプリの最低限の UI Test しか書いてませんが、何しろ目玉機能は共有メニューからの呼び出しなので、共有メニューからの UI Test も書きたいです。はい。

更にテストと関連するもので、CI/CD 環境も充実にしたいですね。CI 面として現状まだ SwiftLint も入れてないし Danger も入れていません、Bitrise の Workflow でそれらの Step を用意していますがスキップさせてるだけです。早いうちになんとか直したいです。そして CD 面として開発者アカウントの 2FA 問題もあってなぜか AppStoreConnect にログインできなくなっています。2FA 対応のためのセッション同期やアプリパスワード設定を全部マニュアル通りに設定したはずなのに…というわけで今は Bitrise の方に上げられた .xcarchive ファイルを落として手動で上げています。あとは UI Test で撮ったスクショも自動的にアップロードしていきたいですね。毎回毎回自分で手動で撮って上げるのは流石にだるいです。

機能を追加する

とりあえず今考えているのは履歴機能、お気に入り機能と QR コード画像のカスタマイズ機能です。

履歴と気に入りは文字通り、これまで生成した QR コードの履歴と、お気に入りの QR コードをまとめる機能です。QR コードのカスタマイズ機能は、例えば QR コードの色を変えるとか、ユーザが提供した画像を QR コード画像に組み込むとかの機能です。多分こちらのライブラリーを使った機能かな。

また履歴機能とお気に入り機能に関しては共有メニューからの利用も連動させる予定です。つまり共有メニューから生成したものも履歴に残るし、お気に入りに追加できるようにしたいです。

有料アプリにする(?)

はい、上記の機能を全部組み込んだら有料アプリにしようかなと思っています。オープンソースなのに有料アプリ。

まあもちろんそんな高いアプリにするつもりは全然ないですが、やはりお金が入るのと入らないのとではモチベーションが全然違う(と思う)んですね。

じゃあアプリ内購入にするか広告にすればいいのでは?と思うかもしれませんが、まずアプリ内購入はプログラムをかなり複雑化してしまう(気がする)ので気が進まないです;そして広告はそもそも個人的に嫌いです。せっかくシンプルで使いやすい画面を作ったのにそれを広告で汚したくないです。

逆に言うとつまり今のうちダウンロードしておかないと、今後有料アプリになったらお金払わなくてはならなくなるかもしれませんよ?(悪魔の囁き)(でもそもそも Qiita のユーザならソースコードから自分でフォークしてビルドすればええやん)

他に何か言いたいことある?

実はこのアプリを Slack で宣伝したら、@takasek さんにショートカットで似たようなことができるよーと教えられました。手順としてはまず共有メニューからの入力を受け入れて、その入力から「書類」で QR コードを生成して、更に「書類」で生成された QR コードを Quick Look で表示すればいいです。ただ実際自分でやってみたところ、QR コードの表示はできるが、例えばマップアプリとかの共有内容が複数ある時に、どの QR コードがなんの内容に対応したものかというのは自力で目パースする実際一回誰かにスキャンしてもらわないとわからないのでちょっと大変です。その点このアプリでは共有内容を UISegmentedControl で切り替えられ、更にその内容も UILabel で下に表示しているから分かりやすいです。あと羅小黒戦記マジ面白いからとりあえず日本語字幕版上映中の映画からでいいからみんな見て

参考リンク

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

[Swift5][Firestore]'Failed to get FirebaseApp instance. Please call FirebaseApp.configure() before using Firestore'のエラーを解消

はじめに

iOSアプリにFirestoreを導入する際に、上記のエラーが出ました。エラー内容を直訳すると「'FirebaseAppのインスタンスを取得できませんでした。 Firestoreを使用する前に,FirebaseApp.configure()をコールしてください。」です。
自分が解消できた方法を共有します。

間違ったコード

AppDelegate.swift
import UIKit
import Firebase 

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        FirebaseApp.configure() //ここに書いていた
        let db = Firestore.firestore()
        return true
    }

Firebase公式リファレンスのソースコードではこのような記述方法でしたが、自分のプロジェクトではエラーとなってしまいました。なぜ公式の方法でエラーになってしまったかはまだ分かっていません。

解決方法

AppDelegate.swift
import UIKit
import Firebase

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    override init() { //初期化メソッドを追記
            FirebaseApp.configure()
    }

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        let db = Firestore.firestore()
        return true
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOS13のアップデート対応してみた

久々のQiita投稿です。最近はもっぱらブログに投稿していたので、おざなりになってました。ブログもおヒマなら覗いてみて下さい。
くま's Tech系Blog

最近はiOS13対応をしているので、まとめてメモしたいと思います。

①モーダル表示の変更

最初に一番驚いたのがこれです。

かなり中途半端になっていて、下の画面が少しみえます。

しかも、前面に出ている画面を下げると下の画面が表示されます。これだと、遷移時に更新とかするのに大変です。

Appleはこういうモーダル構成に今後していきたいということですかね?

20200113141248.png

これに関しては、UIModalPresentationStyleにautoが追加され、デフォルトのモードがautoとなっていることで発生しています。(今まではfullScreenがデフォルトでした)

なので、fullScreenにすれば問題ないはずです。

viewController.modalPresentationStyle = .fullScreen

もしくは、stroryboardで設定している場合は変更すればいいはずです。

20200113142003.png

②Segemetの変更

iOS13ではUISegmentedControlの見た目が変更されます。

今まではtintColorを使っていましたが、tintColorに依存しなくなったので、色を設定していてもデフォルトに戻ります。

これは、selectedSegmentTintColorを使うことで変更できます。

文字の色を変更する場合は下記のようにしないといけません。

// 選択されている場合の文字の色
 setTitleTextAttributes([NSAttributedString.Key.foregroundColor: UIColor.white], for: .selected)
// 選択されていない場合
 setTitleTextAttributes([NSAttributedString.Key.foregroundColor: UIColor.red], for: .normal)

また、選択されていない場合の背景色がグレーになっているかと思いますが、これは背景が単色のimageを設定すれば解消されます。(文字の色と合わない場合があるので)

// 背景に単色画像を設定
setBackgroundImage(UIColor.clear.toImage(), for: .normal, barMetrics: .default)
setBackgroundImage(UIColor.red.toImage(), for: .selected, barMetrics: .default)

この場合はtintColorを設定していても.normal.selected両方の設定がないと反映されません。(むしろtintColorはいらない気がします)

③FontのBoldが変更

iOS13でBoldが以前より少し太くなっています。

これによって、ラベルがはみ出したり、全て表示されないといことがあるので、幅を広げたりする必要性がありそうです。(太字を直せるか微妙なところだし、そもそも新しい太字で進めるべきな気がしてきた...)

今のところこんな感じですが、まだまだあります。(ダークモードとか)
そこまで大幅な修正はあまりなさそうですが、2020年4月までにXcode11で開発しないといけないため、そろそろ開発し始めないといけなさそうですね。

参照

iOS13対応をまとめてみた
iOS 13における必須対応について
iOS13でUISegmentedControlの見た目をカスタマイズする
ios13から日本語フォントのboldがより太くなるケース

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