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

日本語表示におけるUILabelのbyCharWrappingとbyWordWrappingの違い

はじめに

Swift の UILabel には折り返しや省略表示を指定するための lineBreakMode というプロパティが用意されています。指定できるモードの中に折り返し位置を単語区切りにする byWordWrapping と、文字区切りにする byCharWrapping があるのですが、byWordWrapping を指定しても日本語の文章は単語区切りで表示されません。それではbyCharWrapping を指定しても byWordWrapping を指定しても違いはないのか、気になったので調べてみました。

環境

Xcode Version 12.0
Swift 5.3

byCharWrapping

lineBreakMode に byCharWrapping を指定して表示したところ画像のようになりました。
わかりやすいようにUILabel の背景を黄色にしています。
image.png

byWordWrapping

次に byWordWrapping を指定して同じ文章を表示してみました。
byWordWrapping を指定すると行頭に句読点や括弧の終わり、小文字、伸ばし棒などがこないように調整されるようです。こちらのほうが読みやすいですね。
image.png

まとめ

byCharWrapping と byWordWrapping のどちらを指定しても良いという場合は禁則処理が考慮されている byWordWrapping を使用するのが良いようです。

■参考サイト
https://developer.apple.com/documentation/uikit/nslinebreakmode

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

Swiftで多次元配列

SwiftのenumはSwiftの言語仕様の中で最高

SwiftのEnumは強力だ。
Swiftのenumとswitch文はSwiftの言語仕様の中でも最も気に入っている。
このenumの存在がなければSwiftなんかとうの昔に飽きている。というくらい強力。
今日はこれを使って複数次元を混合した配列を実装してみた。

MultiDimensionalArray.swift
// MARK: Definition

public enum MultiDimensionalArray<T> {
    case val(T)
    case ary([Self])
}

// MARK: Convinient Initializers

public extension MultiDimensionalArray {

    static func ary(_ values: T...) -> Self {
        .ary(values.map(Self.val))
    }

    static func ary(_ values: Self...) -> Self {
        .ary(values)
    }
}

// MARK: Functional

public extension MultiDimensionalArray {

    func map<U>(_ transform: (T) throws -> U) rethrows -> MultiDimensionalArray<U> {
        switch self {
        case .val(let v):
            return try .val(transform(v))
        case .ary(let a):
            return try .ary(a.map { try $0.map(transform) })
        }
    }

    func flatMap<U>(_ transform: (T) throws -> MultiDimensionalArray<U>) rethrows -> MultiDimensionalArray<U> {
        switch self {
        case .val(let v):
            return try transform(v)
        case .ary(let a):
            return try a
                .map { try $0.flatMap(transform) }
                .reduce(.empty, +)
        }
    }

    func reduce<Result>(_ initialResult: Result, _ nextPartialResult: (Result, T) throws -> Result) rethrows -> Result {
        switch self {
        case .val(let v):
            return try nextPartialResult(initialResult, v)
        case .ary(let a):
            return try a.reduce(initialResult) { accum, next in
                try next.reduce(accum, nextPartialResult)
            }
        }
    }
}

// MARK: Monoid

public extension MultiDimensionalArray {

    static func + (lhs: Self, rhs: Self) -> Self {
        switch (lhs, rhs) {
        case (.ary(let a), .ary(let b)):
            return .ary(a + b)
        case (_, .val(_)):
            return lhs + .ary([rhs])
        case (.val(_), _):
            return .ary([lhs]) + rhs
        }
    }

    static var empty: Self {
        .ary([])
    }
}

// MARK: Properties

public extension MultiDimensionalArray {

    var count: Int {
        switch self {
        case .val(_):
            return 1
        case .ary(let a):
            return a.count
        }
    }

    var flatCount: Int {
        switch self {
        case .val(_):
            return 1
        case .ary(let a):
            return a.map(\.flatCount).reduce(0, +)
        }
    }

    var depth: Int {
        switch self {
        case .val(_):
            return 0
        case .ary(let a):
            return (a.map(\.depth).max() ?? 0) + 1
        }
    }

    var flatten: Self {
        flatMap(Self.val)
    }

    var flattenArray: [T] {
        flatten.map { [$0] }.reduce([], +)
    }
}

// MARK: Index Accessibility

extension MultiDimensionalArray {

    subscript(_ index: Int) -> Self {
        self[safe: index]!
    }

    subscript(_ indices: [Int]) -> Self {
        self[safe: indices]!
    }

    subscript(safe index: Int) -> Self? {
        self[safe: [index]]
    }

    subscript(safe indices: [Int]) -> Self? {
        switch (self, indices.splitted) {
        case (.ary(let a), .some(let t)):
            return a[t.head][safe: t.tail]
        case (_, .none):
            return self
        default:
            return .none
        }
    }

    subscript(_ indices: Int...) -> Self {
        self[indices]
    }

    subscript(safe indices: Int...) -> Self? {
        self[safe: indices]
    }
}

// MARK: Description

extension MultiDimensionalArray: CustomStringConvertible {

    public var description: String {
        switch self {
        case .val(let v):
            return "\(v)"
        case .ary(let a):
            return "[\(a.map(String.init).joined(separator: ", "))]"
        }
    }
}

// MARK: Equatable

extension MultiDimensionalArray: Equatable where T: Equatable {}

private extension Array {

    var splitted: (head: Element, tail: [Element])? {
        guard let f = first else { return .none }
        return (f, dropFirst().map { $0 })
    }
}

使い方を説明するのも面倒なのでサンプルコードを書いてみた。

TEST
let a: MultiDimensionalArray<Int> = .ary([.val(1), .val(2), .ary([.val(3), .val(4), .ary([.val(5), .val(6), .ary([]), .val(7)])]), .val(8)])
let b: MultiDimensionalArray<String> = .ary(.ary("apple", "banana"), .ary(.ary("gorilla", "ziraph")), .val("gorilla-gorilla-gorilla"))
let c: MultiDimensionalArray<String> = .val("zebra")

print(a)
print(a.map { $0 * 2 })
print(a.flatMap { .val($0 * 2) })
print(a.flatMap { .ary([.val($0), .val($0 * 2)]) })
print(a.flatMap { .ary([.ary([.val($0), .val($0 * 2)])]) })
print(MultiDimensionalArray.val(10).reduce(5, +))
print(a.count)
print(a.flatCount)
print(a.depth)
print(MultiDimensionalArray<Int>.ary([.ary([])]).depth)
print(MultiDimensionalArray.val(true).flatMap(MultiDimensionalArray.val))
print(MultiDimensionalArray.ary([.val(false)]).flatMap(MultiDimensionalArray.val))
print(b.map { $0.uppercased() }.flattenArray)
print(c.map { $0.uppercased() }.flattenArray)
print(b[1, 0, 1])
print(a == a)
print(b != (b + b))

ちなみに出力はこんな感じ。

Console
[1, 2, [3, 4, [5, 6, [], 7]], 8]
[2, 4, [6, 8, [10, 12, [], 14]], 16]
[2, 4, 6, 8, 10, 12, 14, 16]
[1, 2, 2, 4, 3, 6, 4, 8, 5, 10, 6, 12, 7, 14, 8, 16]
[[1, 2], [2, 4], [3, 6], [4, 8], [5, 10], [6, 12], [7, 14], [8, 16]]
15
4
8
4
2
true
[false]
["APPLE", "BANANA", "GORILLA", "ZIRAPH", "GORILLA-GORILLA-GORILLA"]
["ZEBRA"]
ziraph
true
true

おまけ

ちなみに以前、enumで普通に配列を実装してみた。
こんな感じだった。

List.swift
// MARK: List
public enum List<Element> {
    case empty
    indirect case cons(Element, List<Element>)
}

// MARK: - Initializers
public extension List {
    init(_ elements: Element...) {
        self = .init(from: elements)
    }

    init<T: Collection>(from collection: T) where Element == T.Element {
        self = collection.reversed().reduce(.empty) { .cons($1, $0) }
    }
}

// MARK: - Implement Collection (Essential)
extension List: Collection {
    public func index(after i: Int) -> Int { i + 1 }

    public var startIndex: Int { 0 }

    public var endIndex: Int { count }

    public var count: Int {
        switch self {
            case .cons(_, let xs):
                return xs.count + 1
            case .empty:
                return 0
        }
    }
}

// MARK: - Implement Collection (Additional)
extension List {
    public __consuming func dropFirst(_ k: Int = 1) -> List<Element> {
        return self[from: k]
    }

    public __consuming func reversed() -> List<Element> {
        reduce(.empty) { .cons($1, $0) }
    }

    public func map<T>(_ transform: (Element) throws -> T) rethrows -> List<T> {
        guard case let .cons(x, xs) = self else { return .empty }
        do { return .cons(try transform(x), try xs.map(transform)) }
        catch { return .empty }
    }

    public func flatMap<T>(_ transform: (Element) throws -> List<T>) rethrows -> List<T> {
        guard case let .cons(x, xs) = self else { return .empty }
        do { return try transform(x) + xs.flatMap(transform) }
        catch { return .empty }
    }
}

// MARK: - Implement subscript
public extension List {
    subscript(index: Int) -> Element {
        let elem = self[safe: index]
        precondition(elem != nil, "Out of bounds")
        return elem!
    }

    subscript(safe index: Int) -> Element? {
        guard case let .cons(x, xs) = self else { return .none }
        return index > 0 ? xs[safe: index - 1] : x
    }


    subscript(from index: Int) -> List<Element> {
        guard index > 0 else { return self }
        guard case let .cons(_, xs) = self else { return .empty }
        return xs[from: index - 1]
    }
}

// MARK: - Implement Equatable
extension List: Equatable where Element: Equatable {
    public static func == (lhs: List<Element>, rhs: List<Element>) -> Bool {
        lhs.count == rhs.count && zip(lhs, rhs).reduce(true) { $0 && $1.0 == $1.1 }
    }
}

// MARK: - Implement CustomStringConvertible
extension List: CustomStringConvertible {
    public var description: String {
        return map { "\($0)" }.joined(separator: ", ")
    }
}

// MARK: - Operator Support
public func +<T>(lhs: List<T>, rhs: List<T>) -> List<T> {
    guard case let .cons(x, xs) = lhs else { return rhs }
    if case .empty = xs { return .cons(x, rhs) }
    return .cons(x, xs + rhs)
}

複数の要素を入れておく箱としてなら最初のこの4行だけで十分完成している。

public enum List<Element> {
    case empty
    indirect case cons(Element, List<Element>)
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOS14でのデフォルトブラウザ/メーラー変更時のcanOpenURL==false問題の整理と横展開調査結果

前提環境

  • Xcode 11.3.1
  • Swift 5.1.3
  • iOS 14.0 / 14.0.1

問題事象

ポイント:

  • 問題の事象はiOS 13 SDKでビルドしてもiOS 14の端末では発生する。
  • 14.0から発生しているが14.0.1で解消していない(すなわちiOSのバグではなく仕様変更?)。

サンプルコード:

let url = URL(string: "https://qiita.com")!
guard UIApplication.shared.canOpenURL(url) else {
    return
}
UIApplication.shared.open(url, options: [:], completionHandler: nil)
  • iOS 13以下で上のコードを実行すると、Safariが起動する。
  • iOS 14.0 / 14.0.1でデフォルトブラウザをSafari以外(Chromeなど)に設定し上のコードを実行すると、何も起こらない。
  • iOS 14.0 / 14.0.1でデフォルトブラウザがSafariに設定されていれば、iOS 13以下と同様にSafariが起動する。

<デフォルトブラウザをChromeに変更する手順>
1. iOS 14の端末で、Chromeをインストールする。
2. 設定>Chromeを選択する。
3. ブラウザの一覧が表示されるので、Chromeを選択する

原因

iOS 14.0以降でデフォルトブラウザをSafari以外(Chromeなど)に設定すると、UIApplication.shared.canOpenURL()がfalseを返却するため。

回避策

Info.plistのLSApplicationQueriesSchemesに"https"および"http"を設定すると回避できます。

関連事象

iOS 14においてはデフォルトメーラーについても変更できるようになりました。
"mailto"についても同様に、LSApplicationQueriesSchemesに追加しないと、デフォルトメーラーが変更されている場合にはUIApplication.shared.canOpenURL()がfalseを返却します。

横展開調査

以上の事項は、文末の「参考リンク」の記事で得られた有益な情報です。
執筆者の方には深く感謝を申し上げたいです。

一方この項は、私が独自に追加調査をした結果になります。

電話アプリの"tel"および"telprompt"スキーム

"tel"はApple URL Schemesにてドキュメント化されているスキームです。
"telprompt"はドキュメント化されていないスキームです。

いずれも、LSApplicationQueriesSchemesに追加しなくても、iOS 14でも電話アプリが起動します。

ちなみに、「将来、電話アプリのデフォルトが変更できるようになったら?」という疑問があったので、LSApplicationQueriesSchemesに"tel"を追加してみたところ、追加してもちゃんと動作します(当たり前?)。

Walletアプリの"shoebox"スキーム

ドキュメント化されていないスキームです。
LSApplicationQueriesSchemesに追加しなくても、iOS 14でもWalletアプリが起動します。

設定アプリ

こちらのスキームは文字列ではなくUIApplication.openSettingsURLStringとなりますが、(当然ながら)LSApplicationQueriesSchemesに追加しなくても、iOS 14でも設定アプリが起動します。

検証結果の整理

  • iOS 14では、カスタムスキームに加えて、デフォルトを変更できるアプリのスキームもInfo.plistに定義しないとダメです。
  • ドキュメント化されているスキーム、ドキュメント化されていないけど動くスキーム、いずれも、現時点でブラウザとメーラー以外は影響を受けていない模様です。
  • Xcode 12 (iOS 14 SDK)でビルドしたアプリだけではなく、Xcode 11 (iOS 13 SDK)でビルドしたアプリでもiOS 14上での挙動は同じです。

参考リンク

【iOS14】デフォルトブラウザを変更した時にcanOpenURLがfalseになる問題
iOS 14対応で気をつけるべきこと

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

iOS14でデフォルトブラウザ/メーラー変更時にcanOpenURLがfalseになる問題の整理と横展開調査結果

前提環境

  • Xcode 11.3.1
  • Swift 5.1.3
  • iOS 14.0 / 14.0.1

問題事象

サンプルコード:

let url = URL(string: "https://qiita.com")!
guard UIApplication.shared.canOpenURL(url) else {
    return
}
UIApplication.shared.open(url, options: [:], completionHandler: nil)
  • iOS 14.0 / 14.0.1でデフォルトブラウザをSafari以外(Chromeなど)に設定し上のコードを実行すると、ブラウザが起動しない。
  • Xcode 11 (iOS 13 SDK) でビルドしてもiOS 14の端末では発生する。
  • 14.0から発生しているが14.0.1で解消していないということは、すなわちiOSのバグではなく仕様変更?

<デフォルトブラウザをChromeに変更する手順>
1. iOS 14の端末で、Chromeをインストールする。
2. 設定>Chromeを選択する。
3. ブラウザの一覧が表示されるので、Chromeを選択する

原因

iOS 14.0以降でデフォルトブラウザをSafari以外(Chromeなど)に設定すると、UIApplication.shared.canOpenURL()がfalseを返却するため。

回避策

Info.plistのLSApplicationQueriesSchemesに"https"および"http"を設定すると回避できます。

関連事象

iOS 14においてはデフォルトメーラーについても変更できるようになりました。
"mailto"についても同様に、LSApplicationQueriesSchemesに追加しないと、デフォルトメーラーが変更されている場合にはUIApplication.shared.canOpenURL()がfalseを返却します。

横展開調査

以上の事項は、文末の「参考リンク」の記事で得られた有益な情報です。
執筆者の方には深く感謝を申し上げたいです。

一方以下は、私が独自に追加調査をした結果になります。

電話アプリの"tel"および"telprompt"スキーム

"tel"はApple URL Schemesにてドキュメント化されているスキームです。
"telprompt"はドキュメント化されていないスキームです。

いずれも、LSApplicationQueriesSchemesに追加しなくても、iOS 14でも電話アプリが起動します。

ちなみに、「将来、電話アプリのデフォルトが変更できるようになったら?」という疑問があったので、LSApplicationQueriesSchemesに"tel"を追加してみたところ、追加してもちゃんと動作します(当たり前?)。

Walletアプリの"shoebox"スキーム

ドキュメント化されていないスキームです。
LSApplicationQueriesSchemesに追加しなくても、iOS 14でもWalletアプリが起動します。

設定アプリ

こちらのスキームは文字列ではなくUIApplication.openSettingsURLStringとなりますが、(当然ながら)LSApplicationQueriesSchemesに追加しなくても、iOS 14でも設定アプリが起動します。

検証結果の整理

  • iOS 14では、カスタムスキームに加えて、デフォルトを変更できるアプリのスキームもInfo.plistに定義しないとダメです。
  • ドキュメント化されているスキーム、ドキュメント化されていないけど動くスキーム、いずれも、現時点でブラウザとメーラー以外は影響を受けていない模様です。

参考リンク

【iOS14】デフォルトブラウザを変更した時にcanOpenURLがfalseになる問題
iOS 14対応で気をつけるべきこと

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

SwiftのFirestoreで前方一致検索をする

例えばこんなデータがあったとして

let usersRef = Firestore.firestore().collection("Users")
usersRef.document("UserA").setData([
    "name": "UserA",
])
usersRef.document("UserB").setData([
    "name": "UserB",
])

こう書く

let postRef = usersRef.order(by: "name")
    .start(at: [検索したい文字列]).end(at: [検索したい文字列 + "\u{f8ff}"])
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Xcode 12 「Double-quoted include」エラーの対処法

Xcodeを12にアップデートすると、エラーが大量に。。

スクリーンショット 2020-09-26 13.51.09.jpg

アプリをビルドすると、GoogleDataTransport Groupから大量のエラーが発生しました。

- Double-quoted include "pb.h" in framework header, expected angle-bracketed instead
- Double-quoted include "pb_common.h" in framework header, expected angle-bracketed instead
- Double-quoted include "pb_decode.h" in framework header, expected angle-bracketed instead
- Double-quoted include "pb_encode.h" in framework header, expected angle-bracketed instead
-

ダブルクオートが含まれているのが、エラーの原因か??

解決法

project>Pods>Build Setting>Quoted include in... を以下のように変える
スクリーンショット 2020-09-26 13.51.32.jpg

これでDouble-quotedはエラーとみなされなくなりました!

参考

https://github.com/firebase/firebase-ios-sdk/issues/5987

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

【3ヶ月】モバイルエンジニアに転職してみて

こんにちは!

今回は、モバイルアプリエンジニアになって3ヶ月が経とうとしているためその振り返りをしていこうと思います!

簡単な経歴

新卒で大手SIerに入社し、大規模バックオフィスシステムの開発を行なっていました。
ただ、使っている技術がとてもレガシーだったりで、2年間勤めた後に、モバイルアプリ開発を行っているベンチャー企業に転職しました。

詳細はこちらを読んでいただければ…!:bow:
【未経験】SIerからWeb系エンジニアに転職するまでの流れ

今何しているの?

2ヶ月近くあった研修を終え、無事プロジェクトにアサインされSwiftでアプリの保守をやっています。
研修の基礎知識はもちろん必要ですが、それ以外にもライブラリ周りの知識やFirebaseの知識が必要で大変苦戦しております。。。

基本的はすでにリリース済みのアプリに対して、リファクタリングやiOS14での動作確認を最近はしています!

研修はどうだったの?

思ったより、研修時間がたっぷりあり(約2.5ヶ月)、本当に成長できたと思います!(研修も十分になく、すぐにアサインされるんだろうなって思っていました。。。)

研修の形式は、AndroidとiOSでそれぞれ指定されたライブラリを用いてアプリを作るもので、基本的には1日中コードを書いていました。

完成したらプルリクを出して、エンジニアの方にレビューをいただくのですが、それが本当に優しくてとてもやりやすかったです!!
プルリクのconversationが200を超えてしまうこともあり、とても細かくレビューしていただけました。

私自身、Swiftは全くの未経験だったため、とても不安がありましたが、周りの方に適宜質問したりすることでなんとか形にすることができました。

また、毎日新しい学びがあるためとてもモチベ高く取り組むことができました。

やっておけばよかったこと

アウトプットの習慣化

ちょくちょくQiitaに記事を投稿していたのですが、もっと頻度を上げてアウトプットするべきだったと思いました。
基礎中の基礎でも、何度も同じ記事をみにいくことがあった(画面遷移のコードとか)のでそういった内容を自分の中でまとめて記事にできていればもっと理解を深められたのではないかと思いました。

自分の場合は、早く業務につきたすぎて早く研修を終わらせることに集中してしまったので後悔しています。

これからアウトプットの頻度を上げていきたいですね…!

ターミナルのカスタマイズ

すごい初歩的なことですが、入社するまでこんなことができるなんて知らなかったのでここら辺はある程度知っておいた方がいいかと思いました。
⌘ + Eで過去に行ったことのあるディレクトリに行けたり、⌘ + Rで過去に使ったコマンドをすぐに使えたりするのでとても便利です。

zshpecoで調べるといろいろ出てきます

zinitでzsh環境をいい感じにしたメモ

大変だったこと

質問のしかた

これは昔から自分が苦手っていうのもあるのですが難しい。。。

自分がどこまで理解しているのか、どこまで試したのかまとめた上で質問したいけどそこまでもわからないことが多々ありますね

特に知らない技術を使う場合は難しい…けど下手ながらも知識は後から追いついてくると信じて質問して教えていただいてます…(ありがたい:laughing:

質問すると、「これはNotificationCenter使わないとね」とか「ライブラリ周りだから様子見かな」ってこともあり時間を浪費してしまうこともあるのでやっぱり早めにくことが大事だなあと!

リリース済みのアプリへのジョイン

基本的にわからないことはググれば出てくるのですが、そもそも何からしたらいいのかわからず作業が止まってしまうことがありました。
すでにリリースされているアプリの保守にジョインするため、この動作はどこのファイルのどの箇所なのかわからなかったりします。

特に、新しい機能の追加は意外とあっさりできるのですが、既存部分との整合性を合わせるのがとても大変です。

そういった時は、結構早めに周囲に相談・報告するようにしています。本当に周りのかたが優しいので大遅延して迷惑をかけないように早めに甘えています。

やっぱりGitは難しい

入社前に個人開発でGitは使っていたのですが
- git add -A
- git commit -m
- git status
- git log

くらいしか使ってなかったので苦戦しました。
研修中にコンフリクトを解消するために間違えてmasterにPR中のブランチをコミットしてしまったりしてしまいました(研修でよかったよ:fearful:

個人開発だと、PRとか使うことはあまりないと思いますが
- git cherry-pick
- git stash

とかは個人で使っておいて慣れておいた方がいいかと思いました!あとはコンフリクトの解消の仕方とか!

結局転職してどうだった?

まだ転職して3ヶ月ですが、今のところ転職して大正解でした!

毎日、学びがありますし、周りの方は親切なのでモチベーション高く仕事できています!
コードが描けなかったり、エラーが消えない時はめっちゃ苦しいですがそれ以外は楽しく仕事できています:grin:

また、自分で改修したアプリが実際に自分のiPhoneで動作しているのを見ると嬉しくなりますね:raised_hand:

これから業務に慣れてきたら、会社の組織づくりの部分にも参加していきたいと考えています。採用か教育かな〜笑

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

iOS14におけるデバッグメッセージを記録するためのOSフレームワークの使用

ご存じの通り、printを利用するとログメッセージをXcodeのコンソール内に表示させることができますが、printではログメッセージは一切保存されません。

しかしながら、printコマンドで表示されたメッセージに含まれているのはテキストのみです。ログメッセージをさらにオーガナイズしたい場合には、os フレームワークの使用が可能です。

この記事では、iOS 14におけるSwiftでの os フレームワークの使用について、およびログメッセージを読むための Console.app アプリの使用について説明します。

os フレームワークの使用

iOS 14では、os フレームワークが向上します。

まずは、ログインスタンスを初期化します

let logger = Logger(subsystem: "com.example.OSLogExample", category: "OSLogExample")

Console.app アプリケーション内で名前を検索することでログメッセージを簡単に見つけることができるように、subsystemcategory に一意の名前を指定する必要があります。

ログレベル

5つのログレベルがあります。

説明 Xcode にリアルタイムで表示されますか。 Console.app にリアルタイムで表示されますか。
.debug 一般的なデバッグメッセージのため はい いいえ
.info 情報ログメッセージのため はい はい
.notice 通知メッセージのため はい はい
.error エラーメッセージのため はい はい
.fault 障害メッセージのため はい はい
@State var publicLogContent: String = "パブリックログコンテンツ"
@State var privateLogContent: String = "プライベートログコンテンツ"
//.debug
logger.debug("これはデバッグログです: \(self.publicLogContent, privacy: .public) \(self.privateLogContent, privacy: .private)")
//.log
logger.log("これは情報ログです: \(self.publicLogContent, privacy: .public) \(self.privateLogContent, privacy: .private)")
//.notice
logger.notice("これは通知ログです: \(self.publicLogContent, privacy: .public) \(self.privateLogContent, privacy: .private)")
//.error
logger.error("これはエラーログです: \(self.publicLogContent, privacy: .public) \(self.privateLogContent, privacy: .private)")
//.fault
logger.fault("これは不具合です: \(self.publicLogContent, privacy: .public) \(self.privateLogContent, privacy: .private)")

Console.app アプリケーション内にプライベートコンテンツ (privacy: .private) が非表示になります

全てのメッセージのタイプに関して、お持ちのデバイスがコンピューターにつながっている場合、メッセージはXcodeウィンドウで見られます。

image

.info.notice.error及び .fault については、お使いのデバイスがコンピューターに接続されており、Xcodeコードによってローンチされている場合、Console.app アプリケーション内に現れるメッセージを確認できます。

スクリーンショット 0002-09-26 2.28.05.png

Xcodeでアプリが起動していない状態で、デバイスをコンピュータに接続すると、プライベートコンテンツが非表示になります。

スクリーンショット 0002-09-26 2.28.39.png

Macで Console.app アプリを使用する

Macで Console.app を開き、iPhoneから送られてくるログメッセージを読むことができます。場所は アプリケーション > ユーティリティ > コンソール です。

次に、カテゴリ名 category を入力します(この例では OSLogExample です)。左の矢印アイコンをクリックして、検索条件を「プロセス」に変更します。

スクリーンショット 0002-09-26 2.25.14.png

⭐️ こちらのウェブページにアクセスすると、私の公開されているQiitaの記事のリストをカテゴリー別にご覧いただけます。

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

AVFoundationでカメラを使う最小構成

毎回調べている気がするので。

必要なプロパティ。

var captureSession = AVCaptureSession()
var previewView = UIView()
var previewLayer:AVCaptureVideoPreviewLayer?

var videoDataOutput = AVCaptureVideoDataOutput() // VideoDataの場合
var photoOutput = AVCapturePhotoOutput()// PhotoDataの場合

設定。

let device = AVCaptureDevice.default(for: AVMediaType.video)
let deviceInput = try! AVCaptureDeviceInput(device: device!)

captureSession.addInput(deviceInput)
captureSession.addOutput(videoDataOutput)

previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
previewLayer?.connection?.videoOrientation = AVCaptureVideoOrientation.portrait
previewView.layer.addSublayer(previewLayer!)

captureSesion.startRunning()

撮影。

VideoDataの場合、デリゲートメソッド内でフレームを取得。

func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
    let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)
}

Photoの場合、capturePhotoしてデリゲートメソッド内で処理。

self.photoOutput?.capturePhoto(with: AVCapturePhotoSettings(), delegate: self)
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
    if let imageData = photo.fileDataRepresentation() {
        let uiImage = UIImage(data: imageData)
    }
}

お仕事のご相談こちらまで
rockyshikoku@gmail.com

Core MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。

Twitter
MLBoysチャンネル
Medium

相棒
note
相棒
note

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

動物を認識して自動撮影するカメラを作る

Sep-26-2020 02-43-16.gif

犬・猫がフレーム内に現れたら自動でシャッターを切る機能を作ります。

手順

Visionに動物認識コンピュータービジョンリクエストがあります。
リクエスト結果でフレームに動物がいるか判別し写真を撮るように設定します。

let animalRequest:VNRecognizeAnimalsRequest = {
    let request = VNRecognizeAnimalsRequest(completionHandler: { (request, error) in
        guard let animalObservation = results.first as? VNRecognizedObjectObservation else { return }
// animalObservationがあれば、動物がいるので、シャッターを切る
        self.avCapturePhotoOutput.capturePhoto(with: settings, delegate: self as! AVCapturePhotoCaptureDelegate)
    })
    request.revision = VNRecognizeAnimalsRequestRevision1 //リビジョン1では、認識できるのは犬・猫のみです。
    return request
}()

captureOutputデリゲートメソッド内で、上記のリクエストを実行し、フレームを解析します。

func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
    guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return }
    let imageRequestHandler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer, orientation: orientation, options: [:])
    do {
        try imageRequestHandler.perform([animalRequest])
       } catch {
        print(error)
       }
}

応用

同じ手法で他のリクエストを使うと、人間や任意の物体を認識して写真を撮れます。
Observationには、犬猫の場所を示すBoundingBoxも含まれていますので、そこにオートフォーカスしたりもできます。


お仕事のご相談こちらまで
rockyshikoku@gmail.com

Core MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。

Twitter
MLBoysチャンネル
Medium

相棒
note
相棒
note

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

ARKitとAVFoundationは何秒で切り替えられるのか

ARKitとAVFoundation両方の恩恵を受けたい

Gif:ARKitを中断してAVFoundationでキャプチャ、すぐARに戻る
Sep-26-2020 00-42-43.gif

ARKitの世界・顔認識のトラッキングを使いながら、高画質の画像をキャプチャしたいこともあるかもしれません。
ARKitでは1920*1440が最高サイズなので、「AVFoundationでキャプチャすればいいんじゃね?」「ARKitとAVFoundation同時に使えるのかな?」と僕はなりました。

調べたところ、
ARKitとAVFoundationのセッションは、同時には立ち上げられません。
では、素早く切り替えれば、何秒かかるのか。

やってみました。
*iPhone11 iOS14で実験。

実験手順

トラッキング状態を保存して、ARセッションを一時停止し、AVCaptureSessionをスタート。

sceneView.session.getCurrentWorldMap { [self] worldMap, error in
    time = 0.0 // 裏でTimerでtimeを加算しています。0に戻してここからスタート。
    sceneView.session.pause() 
    map = worldMap
    avCaptureSession.startRunning() //AVFoundationスタート
}

実験1、AVCaptureVideoDataOutputで画像を取得する

captureOutputデリゲートメソッド内で画像を取得し、一枚撮ったらすぐにAVCaptureSessionを止めて、ARセッションを再開。

func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
    let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)
    let image = UIImage(ciImage: CIImage(cvImageBuffer: pixelBuffer!))
    print(time)
    avCaptureSession.stopRunning()
    let configuration = ARWorldTrackingConfiguration()
    configuration.initialWorldMap = map
    sceneView.session.run(configuration, options: [])
}

0.15秒でキャプチュア。

しかし、AVFoundationでキャプチャした画像は、セッション立ち上げ直後なので暗い。
最初5フレーム落としてキャプチャしたところ、
綺麗に3840*2160で撮れて0.29秒でした。
画面の停止は0.5秒程度(シャッターを切るくらい)の体感でした。

実験2、AVCapturePhotoCaptureで画像を取得する

写真用のアウトプットで撮ってみました。

func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
    if let imageData = photo.fileDataRepresentation() {
        let uiImage = UIImage(data: imageData)
        print(time)
        // 0.50
        self.captureSession.stopRunning()
        let configuration = ARWorldTrackingConfiguration()
        configuration.initialWorldMap = map
        sceneView.session.run(configuration, options: [])
}

こちらは綺麗に写真が撮れてキャプチャまで0.5秒でした。
しかし、写真のシャッターを切ってしまうと、ARKitの復帰が遅く、7秒程度画面が止まったままでした。
Sep-26-2020 00-46-53.gif

結果

キャプチャの仕方 キャプチャまでの秒数 画面停止秒数
AVCaptureVideoDataOutput 0.3 0.5
AVCapturePhotoCapture 0.5 7.0

結論

実用には、VideoDataOutputで最初の数コマ落として撮るのがギリギリ、普通のシャッターぐらい中断感覚で使えるかなあ、という意見です。

ちなみに、ARWorldTrackingでデバイスの傾きデータをとったところ、セッション切り替え以前と以後で0.05ラジアン程度ずれていました。デバイス固定したらズレなかったので、キャプチャしている間の0.3秒の僕の手ブレがそれくらいということですね。

追記:ARPositionalTrackingConfigurationというデバイス位置をとるだけの構成があって、ワンチャンAVFoundationと併用できんじゃね?とやってみたら無理でした。


お仕事のご相談こちらまで
rockyshikoku@gmail.com

Core MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。

Twitter
MLBoysチャンネル
Medium

相棒
note

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

[Swift5, Xcode12]CoreDataのMigration方法(忘備録)

はじめに

本記事は、自分の行った作業の忘備録です。CoreDataのマイグレーションとデータ移行の設定の際に、色々とハマったため、忘れないように記録として残しておこうと思って執筆致しました。また、私と同じ境遇でつまづいている方の解決策になればよいとも思っております。

開発環境

  • Xcode12
  • Swift5

背景

iPhoneアプリのアップデートを開発しテストした。

前バージョンからアップデートして実行した際にエラーが出た。

データベースのエンティティの構造を変更していた為、どうやら、CoreData周りで何かしらの作業が必要であることが判明

概要

必要な手順

CoreDataはエンティティを変更した際に、以下の作業が必要になる。

  1. データ構造が変更されたことを、CoreDataに明示する作業。
  2. 変更前のデータを変更後にも使用する場合には、変更前のデータの値と変更後の値とのマッピング(値の引き継ぎ)を明示的に宣言する作業。

1.を行わなかった場合、アプリ起動時にエラーが発生する。
2.を行わなかった場合、アプリを起動して新しいデータ構造に更新された場合に、以前保存されていたデータは引き継がれない。

実施方法

この1.と2.を実施する方法を調べた結果、2つのやり方で実施できるらしい。

① 自動でデータ構造の更新とマッピングを行う方法
② 明示的に宣言してデータ構造更新とマッピングを行う方法。

①に関しては、見聞きしただけなので実際に実施できるのかは不明。もしかしたら、そもそも②の方法しか存在しない可能性がある。

今回自分が実施したのは②の方法のみであるため、ここから先は②の方法でCoreDataのマイグレーション、マッピングを行った具体的な内容を紹介する。

具体的な実施方法

具体的なマイグレーション、マッピング方法は主に3つの手順で実施する。

1. xcdatamodelファイルの作成
2. xcdatamodelのバージョンを設定
3. Data Mapping Modelファイルを作成

手順1 新たなxcdatamodelファイルを作成

手順の前に、サンプルプロジェクトを作成し、データモデルを下記の画像のように設定する。
スクリーンショット 2020-09-24 23.50.00.png

xcdatamodelを選択してから上のEditor -> Add Model Versionを押下する。
スクリーンショット 2020-09-24 23.56.33.png

バージョンネームを指定されるので、適切なネームを入力。今回はExample2とする。
スクリーンショット 2020-09-24 23.59.15.png

finishを押下すると、xcdatamodelのファイルにExpand Iconが出現するので開く。
すると、以前あるExample.xcdatamodelともう一つ、先ほど作成したExample2.xcdatamodelが作成されているのが確認できる。また、データ構造もExample.xcdatamodelと同じ構成で作成されている。
スクリーンショット 2020-09-25 0.00.41.png

手順1の新たなxcdatamodelファイルを作成はこれで以上。
手順2に進む。

手順2 xcdatamodelのバージョンを変更

xcdatamodelを選択して、右側のウィンドウ内の資料マークを選択する。
Model Versionという欄があるのでそこをExampleからExample2へ変更する。
スクリーンショット 2020-09-25 0.05.51.png

xcdatamodelId内のファイルの緑色のチェックマークがExampleからExample2へ変更されているのを確認する。
スクリーンショット 2020-09-25 0.07.26.png

手順2のxcdatamodelのバージョン変更はこれで以上。
手順3へ進む。

手順3 Data Mapping Modelファイルを作成

次に、xcdatamodelファイルを選択してから、File -> New -> File...を選択する。
スクリーンショット 2020-09-24 23.51.18.png

欄の真ん中あたりにあるMapping Modelを選択する。
スクリーンショット 2020-09-24 23.52.55.png

マッピングの元となるソースデータモデルを選択してくださいと出てくるので、表示されているxcdatamodelを選択して、Nextを押下する。選択するのはチェックマークがないExample.xcdatamodelである。
スクリーンショット 2020-09-25 0.12.07.png

Nextを押下した後に、今度はマッピング先となるデータモデルの選択を問われるので、チェックマークがついているExample2.xcdatamodelを選択してNextを押下する。
スクリーンショット 2020-09-25 0.12.34.png

マッピングファイルのファイル名を問われるので、適切に入力する。
今回はExample1toExample2.xcmappingmodelと入力した。その他の入力欄はデフォルトのままで、最後にCreateを押下する。
スクリーンショット 2020-09-25 0.17.19.png

先ほど設定したファイルがプロジェクトフォルダ内に作成されていることを確認できる。ちなみに、このファイルの中身を補足する。
おそらく同じAttributeネームでデータのマッピングを行っていると思われる。つまりExample.xcdatamodelのidデータはAttributeネームが同じExample2.xcdatamodelとマッピングを行うように、自動的にXcodeが設定してくれているのであろう。
スクリーンショット 2020-09-25 0.20.45.png

これで、手順3 Data Mapping Modelファイルを作成は以上。
手順はこれで以上になる。これで、プロジェクトを実行するとCoreDataが更新され、以前の構造で保存されていたデータも引き継きが行われる。

今回ご紹介した方法での実施による懸念点

この方法でデータモデルを更新した際に、考えられるのは
更新するたびに新しいxcdatamodelファイルとxcmappingmodelファイルの2つが作成されてしまう
ということ。
データモデルを頻繁に更新することがあるとプロジェクトフォルダ内がデータモデル関係のファイルで一杯になり、管理のしづらいというデメリットがある。

そのデメリットを解消できるやり方として、冒頭で明記させていただいた方法①が有用なのではないかと考えた。

もう一つのMigration方法

もう一つの方法①はプログラムを改修して、自動的にデータモデルの更新からマッピングまで行ってもらうようにCoreDataに指示をする方法である。

**注意** この方法は執筆者はまだ、試したことがないので確証はないです。

改修するプログラムは簡単である。

AppDelegate.swiftファイル内のCoreDataに関する記述に以下のAdditional Programを付け加えるだけである。

Appdelegate.swift
// MARK: - Core Data stack

    lazy var persistentContainer: NSPersistentContainer = {
        /*
         The persistent container for the application. This implementation
         creates and returns a container, having loaded the store for the
         application to it. This property is optional since there are legitimate
         error conditions that could cause the creation of the store to fail.
        */
        let container = NSPersistentContainer(name: "Example")

        // ------------- Additional Program ------------------
        let description = NSPersistentStoreDescription()
        description.shouldMigrateStoreAutomatically = true
        description.shouldInferMappingModelAutomatically = true
        container.persistentStoreDescriptions = [description]
        // ---------------------------------------------------

        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                // Replace this implementation with code to handle the error appropriately.
                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.

                /*
                 Typical reasons for an error here include:
                 * The parent directory does not exist, cannot be created, or disallows writing.
                 * The persistent store is not accessible, due to permissions or data protection when the device is locked.
                 * The device is out of space.
                 * The store could not be migrated to the current model version.
                 Check the error message to determine what the actual problem was.
                 */
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        return container
    }()

記述もわかりやすいが、単純にプログラムで、自動的にマイグレーションを行うことと、自動でマッピングも行うということを命令するだけである。

これなら、データモデル更新によって、プロジェクト内のフォルダが膨大になってしまうことはないかと思われる。
まだ、この方法で実際にできるか執筆者は確認はしていないが、大きな変更がなければもう一つの方法が手っ取り早いのかなとも感じる。

以上。

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