20200715のSwiftに関する記事は9件です。

特定の文章から URL を削除(置換)する

やること

I like http://hoge.com and https://piyo.net.

この文章から URL を削除して

I like and .

にしたいと思います。

Playground
import UIKit

var input = "I like http://hoge.com and https://piyo.net."
let detector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
let matches = detector.matches(in: input, options: [], range: NSRange(location: 0, length: input.utf16.count))

var urls: [String] = []
for match in matches {
    guard let range = Range(match.range, in: input) else { continue }
    let url = input[range]
    urls.append(String(url))
}
print(urls)
urls.forEach{ url in
    input = input.replacingOccurrences(of: url, with: "")
}
print(input)
output
["http://hoge.com", "https://piyo.net"]
I like  and .

できました。

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

【Swift】Stringを空文字でsplitしたい!

こういうやつ

JavaScriptやRubyなら簡単。

"Hello, World".split('') // -> ["H", "e", "l", "l", "o", "," ...]

String#split(separator: Character)じゃダメなの?

"Hello, World".split(separator: "")
// Error: Cannot convert value of type 'String' to expected argument type 'String.Element' (aka 'Character')

そもそも実行できない。

String#components(separatedBy separator: T)

"Hello, World".components(separatedBy: "") // -> ["Hello, World"]

お、惜しい!(惜しくない)

正解 → Array(_ s: Sequence)

Array("Hello, World") // -> ["H", "e", "l", "l", "o", "," ...]

というわけで、Arrayのイニシャライザが使えるようです。
いざ調べても、たどり着くのに苦労したので記事にしてみました。

参考

Convert Swift string to array
https://stackoverflow.com/questions/25921204/convert-swift-string-to-array

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

【Swift】JavaScriptやRubyみたいにStringを空文字でsplitしたい!

こういうやつ

JavaScriptやRubyなら簡単。

"Hello, World".split('') // -> ["H", "e", "l", "l", "o", "," ...]

String#split(separator: Character)じゃダメなの?

"Hello, World".split(separator: "")
// Error: Cannot convert value of type 'String' to expected argument type 'String.Element' (aka 'Character')

そもそも実行できない。

String#components(separatedBy separator: T)

"Hello, World".components(separatedBy: "") // -> ["Hello, World"]

お、惜しい!(惜しくない)

正解 → Array(_ s: Sequence)

Array("Hello, World") // -> ["H", "e", "l", "l", "o", "," ...]

というわけで、Arrayのイニシャライザが使えるようです。
いざ調べても、たどり着くのに苦労したので記事にしてみました。

参考

Convert Swift string to array
https://stackoverflow.com/questions/25921204/convert-swift-string-to-array

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

Swiftのみを使って、今Qiitaを作るとしたら

Swift は iOS アプリを作るための言語というイメージが強いと思います。しかし、実際にはサーバーサイドプログラムや機械学習、コマンドラインツールの開発など、 多様な目的で利用できる汎用言語です 。 2015 年にオープンソース化され、 Linux でも動作し、近々 Windows もサポートされる予定です。

10.png

Swift は Apple の言語ですが、それは TypeScript が Microsoft の、 Go が Google の言語だというのと同じ程度の意味しか持たないと思います。 Swift Core Team には Google のエンジニアも入っていますし、新しい言語の機能はすべて、オープンな場で議論された上で決定されます。

そんな Swift にとって期待される二つの分野が、 Web のクライアントサイドとサーバーサイドです。 WebAssembly に対応することで、今まさに、 Swift で書かれたプログラムがブラウザ上で動く ようになろうとしています。また、 サーバーサイドに関しては Swift のオープンソース化以来 4 年以上かけて進化してきました 。それらをフル活用して、 Qiita のフロントエンドもバックエンドも Swift だけで作るなら ということを考えてみたいと思います。

実際にやろうとすると 2020 年 7 月現在では茨の道ですが、実験的に考えてみるには、あえて Web アプリケーションのフロントエンドもバックエンドも Swift で作るならということを考えてみるのはおもしろい題材です。

フロントエンド

これまで、ブラウザ上で動作するプログラミング言語は JavaScript 一択でしたが1、 WebAssembly によって様々な言語がブラウザ上で動作する未来が見えてきました。

WebAssemblyは、ウェブブラウザのクライアントサイドスクリプトとして動作するプログラミング言語(低水準言語)である。wasmとも称されており、ブラウザ上でバイナリフォーマットの形で実行可能であることを特徴とする。
WebAssembly - Wikipedia

WebAssembly はモダンなウェブブラウザーで実行できる(略)ネイティブに近いパフォーマンスで動作する(略)アセンブリ風言語です。(略) C/C++ や Rust のような言語のコンパイル対象となって、それらの言語をウェブ上で実行することができます。
WebAssembly - MDN Web Docs

Swift も WebAssembly にコンパイルすることで、ブラウザ上で実行することができます。

Swift は WebAssembly の本命になり得る?

僕は、 将来的に Swift は WebAssembly の本命言語になるかもしれない と考えています。

ブラウザ上で C/C++ のコードが動作するようになっても、それらが JavaScript / TypeScript を代替するようになるのは難しいと思います。パフォーマンス上のボトルネックになっている部分を C/C++ で書くという棲み分けになるでしょう。 Rust も素晴らしい言語ですが、フロントエンド用の言語として万人受けするかと言うと疑問が残ります。

Swift はアプリ開発に用いられている言語です。言語仕様的にフロントエンドとの相性は良いでしょう。 フロントエンドエンジニアにとってはとっつきやすいライトな書き心地の言語 だと思います。

一方で、 Swift は C/C++ と遜色ないパフォーマンスを実現可能な言語 です。現時点では、パフォーマンスを追求したときに不利になる部分もありますが、 Rust のような所有権を導入することで、この問題を解決しようとしてます。 Swift 6 (おそらく 2021 年リリースになる次期メジャーバージョン)についてアナウンスされた "On the road to Swift 6" の中でも所有権について述べられており、近い内に利用可能になるのではないかと考えられます2

つまり、 Swift は WebAssembly の特徴であるパフォーマンスと、フロントエンドの書きやすさを両立できる言語だと言えます。

Swift for WebAssembly の現状

2020 年 7 月現在、 Swift は正式には WebAssembly に対応していません。しかし、昨年から今年にかけて急速に進展してきています。

Swift の WebAssembly 対応は、 SwiftWasm というプロジェクトで行われています。 SwiftWasm は Swift Core Team のアドバイスを受けながら、 Swift コンパイラを fork して WebAssembly 対応を進め、本流の Swift リポジトリにパッチを送っています。 SwiftWasm の中心人物の一人である @kateinoigakukun は、その進捗を次のように報告しています。

これによると、昨年時点では標準ライブラリのテストが 20% ほどしか通らなかったのが、 2 月時点で 90% 、 4 月についに 100% 通るようになったと書かれています。また、 Swift Package Manager ( Swift のおける npm や gem のようなものです)の WebAssembly 対応をしたり、 WebAssembly 用のデバッガを開発したり、 SwiftWasm 公式になっている Swift と JavaScript を連携するライブラリ "JavaScriptKit" を作ったり、周辺作業もとんでもない成果です。とてもインターンとは思えません。

そのような成果があり、最近ついに Swift がブラウザ上で動くようになりました。 swiftwasm.org で、実際に Swift のコードをブラウザ上で実行してみることができます。

20.png

@kateinoigakukun は現在 Google Summer of Code ( GSoC )で Swift のリンク時最適化に取り組んでいます(参考: わいわいswiftc #21)。現状の SwiftWasm では、 Swift のコードを実行するために標準ライブラリが丸ごとリンクされてしまうため、 Hello World でも 15MB ものサイズになってしまっています。実用を考えると、このままでは現実的ではありません。リンク時最適化によって利用されていない関数等を削除することで、そのサイズの大部分を削減できると考えられます。夏が終わる頃には、 SwiftWasm はさらに一歩進んでいるかもしれません。

SwiftWasm でオンラインエディタを作る

Qiita はあまり JavaScript で派手に動くタイプのサイトではありません。 SwiftWasm で Qiita のフロントエンドを作るとして、どこについて考えてみるとおもしろいでしょうか。

Qiita のページを一通り考えてみた結果、新規記事の投稿ページにおけるオンラインエディタが一番難しそうだという結論に至りました。オンラインエディタは左側に Markdown でテキストが入力されると、リアルタイムに右側にレンダリング結果を表示する必要があります。 Qiita の中で比較的動的な部分が大きいページです。

30.png

SwiftWasm でオンラインエディタを作るためには、 DOM の操作が欠かせないでしょう。前述の JavaScriptKit を使えば、次のように、 Swift で DOM を操作することができます。

import JavaScriptKit

let document = JSObjectRef.global.document.object!

let div = document.createElement!("div").object!
div.innerText = "Hello, Swift!"
let body = document.body.object!
body.appendChild!(div)

このライブラリは、 Swift が動的型付言語と連携するために導入された Dynamic Member LookupDynamic Callable を用いて実装されており、 Swift から JavaScript の関数やメソッドを呼び出したり、フィールドにアクセスしたりすることができます。そのため、 JavaScript と同じように DOM を操作することが可能です。

さて、Markdown から HTML への変換や、シンタックスハイライティングはどうすれば良いでしょうか。これは、 JavaScript の既存のライブラリの力を借りるのが良いでしょう。たとえば、 markdown-ithighlight.jsを組み合わせて実現するなどが考えられます。 JavaScriptKit のおかげで、これらのライブラリを再発明しなくても済みそうです。

JavaScript のライブラリを使うのは一見「 Swift のみ」に反するように思えるかもしれません。しかし、ここでは Qiita を作るために自分で書かなければならないコードが「 Swift のみ」であるというルールで考えます。そもそも、 Swift は glibc のような C 言語のライブラリに依存していたりします。他の言語も、自身にしか依存していないというケースは少ないでしょう。「〇〇(言語)のみ」を現実的に考えると、そのサービスを作るために自分が書くコードが「○○のみ」であるというルールは妥当に思えます。

Markdown を HTML に変換する部分については、 Ink のような Swift 製 Markdown ライブラリを使うこともできるかもしれません。前述の通り、 SwiftWasm は Swift Package Manager に対応しているので、これらのライブラリの導入も簡単です。3

SwiftWasm と仮想 DOM

フロントエンドのコードを書くに当たって、原始的に DOM が操作できるだけでは力不足です。モダンなフロントエンド開発には、 React のような道具が欠かせないでしょう。

iOS アプリや macOS アプリ開発においては、 2019 年に Apple が SwiftUI を発表しました。 SwiftUI は React の仮想 DOM のような仕組みを持つ UI フレームワークです。徐々に現場でも採用され始めており、今後 iOS アプリ開発を劇的に変えるものと考えられています。僕も、まさに今仕事で SwiftUI を使って iOS アプリを作っています。 SwiftUI の強力さを体感するには、 SwiftUI を使ってゼロからアプリを作り上げていくがおすすめです。

しかし、 SwiftUI をそのまま SwiftWasm で使うことはできません。そこで、 SwiftWasm プロジェクトの一貫として、 SwiftUI 互換を目指す Tokamak というライブラリが開発されました。

たとえば、次のようにして "Increment" と "Reset" ができるカウンターを作ることができます。このコードは import 部分を変えれば完全に SwiftUI でも動作します。

import TokamakDOM

struct Counter: View {
    @State var count: Int = 0

    var body: some View {
        VStack {
            Text("\(count)")
            HStack {
                Button("Reset") { count = 0 }
                Button("Increment") { count += 1 }
            }
        }
    }
}

この Counter は仮想的な View であり、実体は別に存在します。 @State が付与された count が書き換えられると、 body が再実行されて仮想 View が生成され、差分が実体に反映されます。

Counter は、 DOMRenderer を使うことで、 JavaScriptKit で取得/作成した DOM 要素の中にレンダリングすることができます。

let div = document.createElement!("div").object!
let renderer = DOMRenderer(Counter(), div)

また、 SwiftUI には存在しない HTML という View を使って、任意の HTML タグを用いて View を作ることもできます。

たとえば、

<div id="foo">
    <div id="bar">
    </div>
</div>

に相当する View は次のように書けます。

HTML("div", ["id": "foo"]) {
    HTML("div", ["id": "bar"])
}

これらを使えば、↓のようにして Markdown を入力するテキストエリアを作れるでしょう。

import TokamakDOM

struct Editor: View {
    let id: String
    @Binding var text: String

    var body: some View {
        HTML("textarea", ["id": id], listeners: [
            "keyup": { event in
                // textarea の value を取得して `text` に反映
            },
        ])
    }
}

バックエンド

Qiita を作るために必要なバックエンドの構成を考えてみましょう。最低限、次のようなものが必要でしょう。

  • 大量のリクエストをさばく Web サーバー
  • リクエストを Web サーバーに振り分けるロードバランサー
  • スケールするデータベース
  • 定期的にランキングを生成するバッチ処理

AWS を使うとして、 Web サーバーは EC2 インスタンスを必要な数だけ立ち上げて、 ELB でリクエストを振り分ければ良いでしょうか。

データベースには何を使うのが良いでしょう。 Qiita のデータ構造はそれほど複雑ではなさそうですが、一方で、高いスケーラビリティが要求されそうです。リレーショナルデータベースよりも DynamoDB が適しているでしょう。

僕はバックエンドエンジニアではないので自信を持ってこれが良いとは言えないですが、とりあえずそのような構成で考えてみましょう。

Web サーバー

Swift で最もメジャーな Web フレームワークは Vapor であり、 Server-Side Swift (SSS) のデファクトになっています。

40.png

Vapor を動かすのに Amazon Linux か Ubuntu かどちらがいいかの知見は持っていませんが、 Swift は Ubuntu の方がサポート状況が良さそうなので、 Ubuntu + Nginx + Vapor という構成にすることにしましょう。

Vapor を使うとサーバーサイドのコードを次のように書くことができます。 GET でリクエストを受けたときに "Hello, world." とレスポンスを返すだけのプログラムです。

import Vapor

let app = try Application(.detect())
defer { app.shutdown() }

app.get("hello") { req in
    return "Hello, world."
}

try app.run()

(引用元: https://vapor.codes/

また、 Vapor はそれ単体だけではなく、連携して動作するための様々な道具が開発されています。たとえば、 Vapor 公式の Leaf というテンプレートエンジンを使えば、次のようなテンプレートで HTML を出力させることができます。

<h1>#(title)</h1>
#for(number in numbers){
    <p>#(number)</p>
}

(引用元: https://docs.vapor.codes/3.0/leaf/overview/

その他にも Web フレームワークに求められる諸機能はそろっており、 Qiita を実装する上での障害は特にないように思えます。

データベース

Vapor には Fluent という ORM フレームワークがあります。これを使って MySQL や PostgreSQL などにアクセスすることができます。

たとえば、 Qiita の記事を表すモデルクラスを作って、

final class Article: Model {
    static let schema = "articles"

    @ID(key: .id)
    var id: String?

    @Field(key: "name")
    var title: String

    ...
}

次のようにしてデータベースを書き込むことができます。

app.post("articles") { req -> EventLoopFuture<Article> in
    let article = try req.content.decode(Article.self)
    return article.save(on: req.db).map { _ in article }
}

DynamoDB についても、 Vapor 3.0 のドキュメントには非公式の Community Drivers として FluentDynamoDB挙げられています。しかし、最新の Vapor 4.0 のドキュメントでは記述が消えており、また、リポジトリのスターも 4 と、これを使うのも不安が残ります。

DynamoDB にアクセスするには、 Fluent の利用は諦めて AWS SDK Swift を使った方が無難でしょう。僕自身はやったことはないですが、調べてみたところ、 DynamoDB 型の putItem メソッド

func putItem(_ input: PutItemInput, on eventLoop: EventLoop? = nil) -> EventLoopFuture<PutItemOutput>

を使って、次のような感じで記事を書き込むことができそうです。

let dynamoDB: DynamoDB = ...

let item: [String: DynamoDB.AttributeValue] = [
    "title": .s(article.title),
    "tags": .ss(article.tags),
    ...
]
let input = DynamoDB.PutItemInput(item: item, tableName: "articles")
let output = dynamoDB.putItem(input)

↑のコードでは、 item を手で(人間がコードを書いて)作っていますが、 CodableSwift Encoders)を活用すれば、この変換を自動化することができるでしょう。

これで、 Swift から DynamoDB の利用もできました。

タイムラインの生成

Qiita にはフォローとタイムラインの仕組みがあります。タイムラインの実現はデータ構造上、少々やっかいです。一人のユーザーは何百・何千ものユーザーをフォローし得ます。リクエストを受ける度にフォローグラフを元にリアルタイムに記事を JOIN して、しかも時系列順に並べて提示するというのは非現実的でしょう。現実的には、各ユーザーのタイムラインをデータとして保持し、記事が投稿されたらフォロワーのタイムラインにもデータを書き込むなどの設計にする必要があるでしょう4

そうすればリクエストごとに負荷の高い JOIN をしなくて済みます。しかし、記事が投稿されたときには投稿者のフォロワー全員のタイムラインに書き込みが必要です。フォロワーは何百・何千人になるかもしれないので、 1 件の記事の投稿が何百・何千件もの書き込みを必要とするかもしれないということです。

フォロワーが一桁というユーザーが大半でしょうから、一部の人気ユーザーの投稿でそのような大量の書き込みが発生しても、全体の負荷としては誤差の範囲かもしれません。しかし、投稿時に何百・何千件もの書き込みを Web サーバーから行うのは避けたいところです。 AWS Lambda を使って、 DynamoDB への書き込みをトリガーとして タイムラインを生成することにしましょう。

先日( 2020 年 5 月)、 Swift AWS Lambda RuntimeSwift の公式ブログからアナウンスされました。 Swift AWS Lambda Runtime を使った Lambda のコードは次のように書くことができます。

// Import the module
import AWSLambdaRuntime

// in this example we are receiving and responding with strings
Lambda.run { (context, name: String, callback: @escaping (Result<String, Error>) -> Void) in
  callback(.success("Hello, \(name)"))
}

(引用元: https://github.com/swift-server/swift-aws-lambda-runtime/

これを使えば、タイムラインを生成する Lambda のコードも Swift で書くことができそうです。

ランキングの生成

残すはランキングの生成です。アルゴリズムの詳細はわかりませんが、おそらく機械学習を使っているでしょう。

Swift for TensorFlow

Swift と機械学習を語る上で欠かせないものに、 Swift for TensorFlow があります。

Swift for TensorFlow は Google が進めているプロジェクトで、 Swift の生みの親である Chris Lattner が Google 移籍後に立ち上げたものです。 Swift for TensorFlow はとてもおもしろいプロジェクトで、ただの Swift 向け TensorFlow ラッパーではありません。それなら TensorFlow for Swift ( Swift のための TensorFlow)になるはずです。 TensorFlow のための Swift 、つまり、 Swift for TensorFlow は TensorFlow のために fork された Swift コンパイラ です。

Swift for TensorFlow - TFiwS (TensorFlow Dev Summit 2018)

Swift for TensorFlow については上記ビデオと公式ドキュメントで詳しく解説されていますが、簡単に説明すると次のような課題を解決するものです。

Deep Learning フレームワークには、主に Define-and-Run と Define-by-Run の二つの方式があります。 Define-and-Run では最初に(ニューラル)ネットワークを定義し、それからデータが流されます。一方、 Define-by-Run では動的にネットワークを定義します。そのため、条件分岐やループのような制御構文を組み合わせてネットワークを定義することが容易です。パフォーマンスでは Define-and-Run に利がありますが、コードの書きやすさでは Define-by-Run に軍配が上がります。元々 Define-and-Run を採用していた TensorFlow も TensorFlow 2.0 で Define-by-Run をデフォルトにするなど、近年では Define-by-Run が優勢です。

この、 Define-and-Run と Define-by-Run のいいとこ取りをしようというのが Swift for TensorFlow です。 Swift コンパイラに手を入れて、コンパイル時に制御フローやネットワークを解析できるようにすることで、静的にネットワークを構築しながら、 Define-by-Run の書きやすさを実現しようというエキセントリックなプロジェクトです。

また、ニューラルネットワークライブラリに欠かせない自動微分をコンパイル時に行う機能を生み出し、それを Swift の本流に反映させようとしています。この提案は受け入れられ、将来的には Swift に取り込まれる予定です。

さらに、より長大かつ野心的なゴールも見据えているようです。 Swift for TensorFlow はコンパイラの処理に Graph Program Extraction という新たなステージを追加し、コードを解析して TensorFlow のグラフ(棒グラフ等のグラフではなく、グラフ理論のグラフ(ツリーなどを含むデータ構造))を構築します。これを応用すれば次のようなことも可能だと書かれています。

Finally, while TensorFlow is the reason we built this infrastructure, its algorithms are independent of TensorFlow itself: the same compiler transformation can extract any computation that executes asynchronously from the host program while communicating through sends and receives. This is useful and can be applied to anything that represents computation as a graph, including other ML frameworks, other kinds of accelerators (for cryptography, graphics, transcoding, etc), and general distributed systems programming models based on graph abstractions. We are interested in exploring new applications of this algorithm in the future.

(参考訳)最後に、私達がこのインフラを構築したのは TensorFlow のためですが、そのアルゴリズムは TensorFlow 自体からは独立しています。同様のコンパイラによる変換は、送受信を介して通信しながらホストプログラムから非同期的に実行される任意の計算を抽出することができます。これは、グラフとして表すことができる任意の計算に適用し、役立てることができます。そのような計算の例としては、他の機械学習フレームワークや、その他のアクセラレータ(暗号化やグラフィック処理、トランスコードなど)、グラフの抽象化に基づく一般的な分散システムプログラミングモデルなどが挙げられます。私達は、将来的にこのアルゴリズムを適用できる新しい問題を見つけ出すことに関心があります。

Graph Program Extraction - Swift for TensorFlow Design Overview

壮大すぎてくらくらしますが、とても興味深いですね。

この Swift for TensorFlow を使って機械学習をすれば、 Swift だけを使って Qiita のランキングを生成することもできそうです。

Core ML / Create ML

別の手段としては、ランキング生成だけは macOS 上で行い、 Core ML / Create ML の力を借りるということもできます。その場合、 Mac サーバーを用意し、ランキング生成だけそちらで行います。

Core ML は Apple のチップ上で高速にニューラルネットワークの計算することができるライブラリです。 2017 年にリリースされた A11 チップ以降、最近の iPhone や iPad 、これから発売されるだろう Apple Silicon 搭載の Mac には、 Neural Engine と呼ばれるニューラルネットワークの計算のためのハードウェアが搭載されています。これによって、高速かつ高精度な顔認識などが支えられています( Neural Engine が搭載されていなくても、 Core ML は GPU 等を用いて適切に処理を行います)。

また、 Core ML は OS に内蔵されているため、ニューラルネットワークのためのライブラリをアプリに埋め込まなくて良いのもうれしいところです。ちょっと機械学習の成果を使うためだけに、巨大な機械学習ライブラリを埋め込まないといけないのは避けたいでしょう。

Apple の Core ML Tools を用いて TensorFlow 等で構築したモデルを Core ML 用に変換することができます。また、 Apple が Core ML 用に公開しているモデルを使えば、すぐに機械学習を試すこともできます。

50.png

Core ML には学習のための機能は備わっていませんが、 Create ML を用いることで Core ML 用のモデルを生成することができます。

Create ML は一般的な Deep Learning フレームワークとは異なり、自身でネットワークを定義することはできません。入力と出力のペアをデータとして用意すれば、あとは自動的にネットワークが構築され、いい感じに学習してくれます。たとえば、画像の識別であれば、 各クラスごとに最小 10 枚の画像を用意して与えるだけで識別器を生成してくれます 。おそらく、内部で転移学習かファインチューニングを行っているのでしょう。汎用的な Deep Learning フレームワークとするのではなく、一般のエンジニアが手軽に機械学習の恩恵を受けられるツールを提供するという選択が、僕はいかにも Apple っぽいなと思いました。

さて、ランキングを生成するに当たっては、Creating a Model from Tabular Dataが参考になりそうです。これは、画像や動画などではなく、表形式のデータからモデルを訓練するサンプルです。記事のランキングを作るには記事にスコアを付けてそれを予測するモデルを訓練する必要があります。本文を学習するのではなく、 LGTM の数や投稿時刻、直近の LGTM (加速してるか減速してるか)、投稿したユーザーのスコアなどデータを元にスコア付けを行う場合、データはこのような表形式のものとなります。

学習を行うコードは、次のようにして Swift で書くことができます。

// CSV からデータを読み込み
let csvFile = Bundle.main.url(forResource: "MarsHabitats", withExtension: "csv")!
let dataTable = try MLDataTable(contentsOf: csvFile)

// 価格予測に必要なカラムだけを抽出
let regressorColumns = ["price", "solarPanels", "greenhouses", "size"]
let regressorTable = dataTable[regressorColumns]

// 訓練データとテストデータに分割
let (regressorEvaluationTable, regressorTrainingTable) = regressorTable.randomSplit(by: 0.20, seed: 5)

// 学習
let regressor = try MLLinearRegressor(trainingData: regressorTrainingTable, targetColumn: "price")

// 評価
let regressorEvalutation = regressor.evaluation(on: regressorEvaluationTable)

(引用元: https://developer.apple.com/documentation/createml/creating_a_model_from_tabular_data , コメントは著者が付与)

これと同じように記事のスコアを予測する回帰の問題としてモデルを訓練し、得られたモデルを使ってランキングを生成することができます。

なお、ここでは詳細には触れませんが、本文のテキストを元にスコアリングしたい場合は、テキストを入力とするモデルを訓練することもできます。

まとめ

Qiita を作るために必要となりそうな技術要素について、 Swift だけで実現するにはどうすれば良いかを検討してみました。まだまだ開発段階のものも多く、実用には様々なハードルがあるでしょう。しかし、 Swift で様々な領域のコードを書く未来を垣間見ることができたのではないかと思います。 Swift は現在も急速に進化を続けており、 iOS アプリ以外の実プロダクトでも Swift が使える未来が楽しみです!

⚠️ 本記事は、普段使わない技術ばかりを挙げて書いたので、おかしなところがあるかもしれません。マサカリ歓迎です!


  1. 過去には Java Applet や FLASH の ActionScript などもありましたが、 2020 年現在の現実的な選択肢ではないでしょう。 

  2. 一部の機能は _modify などとして、 2020 年 7 月現在でも、すでに試験的に利用可能になっています。 

  3. ただし、本当に SwiftWasm で Ink をビルドできるかは試していません。 

  4. そういうシステムを作ったことがあるわけではないので、本当にこの方法が良いのかはあまり自身がないです。もっと良い方法があるかもしれません。 

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

[Swift] guard let self = self else { return } って何?

じょ

某所で好評を得ているのでこちらに転載します。

これはなに?

以下のようなよくあるサンプルコードのguard let self = self else { return }が何のためにあるのかよくわからない、という疑問にお答えします。

func didTapSignUpButton() {
    Auth.auth().createUser(withEmail: email, password: password) { [weak self] result, error in
        guard let self = self else { return }
        if let user = result?.user {
            // do something
        }
    }
}

短い回答

[weak self] でキャプチャしたselfがクロージャ実行時に存在しているかどうかを確認しています。

長い回答

guard let else

まず、

guard let xxx = yyy else { return }

yyyがOptionalで、yyyに値がなければreturnして値があればxxxにその値を入れる、という構文です。

例えば以下のように動きます。

func hoge(_ value: Int?) {
   gurad let v = value else { return }
   print(v)
}

hoge(10)  // prints 10
hoge(nil) // no prints

変数のキャプチャ

次に、関数による変数のキャプチャというものがあります。

var val = 0

func fuga() {
   val += 2   //  valをキャプチャ
}

print(val)  // prints 0.

fuga()
print(val)  // prints 2.

関数にキャプチャされた変数valは関数内で値を変更することが可能です。

Swiftでは関数とクロージャは同等のものですので以下でも同じです。

var val = 0

let fuga = { val += 2 }

インスタンスのオーナーシップ

最後にクラスのインスタンスのオーナーシップです。
クラスのインスタンスにはデータ領域がありますので、そのインスタンスを使わなくなった時にその領域が解放されなければメモリがひっ迫してしまいます。
しかし、使用中に開放されてしまうと、アプリケーションがクラッシュする事態になります。
このためSwiftではインスタンスにオーナーシップというものを設定しこれに対処しています。
原則として、使用中はオーナーを設定し使用が終わるとそれを外します。オーナーがすべてなくなった段階でインスタンスは解放されます。
これらは自動的に行われており、通常はプログラマは何も考える必要もありません。

ただし、循環参照という状態になるとインスタンスが解放されなくなるため、これを対処する必要があります。

class A {
  let b: B

  init(b: B) {
    self.b = b
    b.a = self
  }
}

class B {
   var a: A?
}

let a: A? = A(b: B())

この例は意味のあるコードではありませんが、循環参照の典型的な例です。
class Aのインスタンスはclass Bのインスタンスを、class Bのインスタンスはclass Aのインスタンスを参照しています。
そのため、上の例でa = nilとしてもa, bどちらのオーナーもなくならないため、どちらも解放されません。

この循環参照を回避するためにweakという修飾子が用意されています。
上の例ではclass Bの変数aweakとします。

class B {
   weak var a: A?
}

このweakという修飾子は「インスタンスは参照するがオーナーとしては登録するな。またインスタンスが解放されたとき値をnilにせよ。」という指示を与えます。
似た修飾子にunownedというものがあります。最後に少し説明をします。

こうすることでa = nilが実行されると、aのオーナーがすべてなくなりaが解放され、それによってbのオーナーもなくなるためbも解放されるようになります。

キャプチャによるオーナシップの獲得

まだまだ続きます。

つぎは今までできたことの複合技、キャプチャによるオーナシップの獲得です。

これは名前の通りです。先ほどのclass Aを使います。

var a: A? = A(b: B())

let f = { print(a) }  // インスタンスaをキャプチャ

a = nil // インスタンスaは解放されない

クロージャがインスタンスaをキャプチャした時、自動的にオーナーが設定されるため、a = nilを実行してもインスタンスaは解放されません。ただしこの場合は、fが使われなくなった時にfと一緒にaも解放されます。

これにも循環参照の問題が発生します。

class C {
  var f: (() -> Void)?
}

let c: C? = C()

c?.f = { print(c) }

c = nil  // c は解放されない

このようにインスタンスcが持つクロージャがcをキャプチャしてしまったため、循環参照が発生し、cが解放されなくなってしまいます。
この時もweak修飾子を使います。ただし、クロージャ自体をweak参照することができないため以下のように行います。

c?.f = { [weak c] in print(c) }

このweakcをキャプチャする時にオーナーを設定するなという指示になります。
この指定をすることでc = nilの時にインスタンスcが解放されるようになります。

ところが、これでも問題が発生する場合があります。class Aを使います。

var a: A? = A(b: B())

let f = { [weak a] in print(a) }

a = nil

f()  // prints nil

aが解放された後にクロージャfを実行しようとすると、クロージャ内で使用しているaがすでに存在していないためnilになってしまいます。

キャプチャしたインスタンスの存在確認

この問題を解決するためには実行前にインスタンスaが存在することを確認する必要があります。
クロージャfを以下のようにします。

let f = { [weak a] in
    gurad let a = a else { return }
    print(a)
}

まず、gurad let a = a else { return }でインスタンスaの存在を確認し、その後それを使用するようにします。

冒頭のサンプルコードにあるguard let self = self else { return }も同じことです。
[weak self]でキャプチャしたselfがクロージャ実行時に存在しているかどうかを確認しています。

おまけ

キャプチャ時のweak指定の副作用

すっかりだまされいるようですね。
書いてあることは正しいのですが、実はguard let self = self else { return }についての説明が足りてません。

guard let xxx = yyy else { return }

yyyがOptionalで、yyyに値がなければreturnして値があればxxxにその値を入れる、という構文です。

と書きましたが、guard let self = self else { return }の右のselfはOptional型ではないはずです。

ところが、

var a: A? = A(b: B())

let f = { [weak a] in print(a) }

これを

let a = A(b: B())

let f = { [weak a] in print(a) }

としたとしても、クロージャ内のaA型ではなくOptional<A>型になります。
これがキャプチャ時のweak指定の副作用です。
weakは「(略)解放されたとき値をnilにせよ」という指示なのでOptional型じゃないと無理なんです。
なので強制的にOptional型になります。

unowned

「循環参照を避けたいけど、weakだとOptionalになって面倒くさい! ここでは絶対インスタンスは存在してるのに!!!」
という時に使うのがunownedです。

weak修飾子は「インスタンスは参照するがオーナーとしては登録するな。またインスタンスが解放されたとき値をnilにせよ。」という指示でしたが、unownedは「インスタンスは参照するがオーナーとしては登録するな」だけの指示を与えます。
nilにする必要がないため、キャプチャ時に強制的にOptional型になることはありません。
アンラップを使うことなくそのまま利用できますので便利です。

ただし、当然のことながらunownedはオーナーを設定しないため利用しようとしたときにインスタンスが解放されてしまっていることがあります。
解放されたインスタンスにアクセスするとプログラムがクラッシュします。
unownedを使う場合はそのことに十分注意しましょう。

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

[Swift] gurad let self = self else { return } って何?

じょ

某所で好評を得ているのでこちらに転載します。

これはなに?

以下のようなよくあるサンプルコードのgurad let self = self else { return }が何のためにあるのかよくわからない、という疑問にお答えします。

func didTapSignUpButton() {
    Auth.auth().createUser(withEmail: email, password: password) { [weak self] result, error in
        guard let self = self else { return }
        if let user = result?.user {
            // do something
        }
    }
}

短い回答

[weak self] でキャプチャしたselfがクロージャ実行時に存在しているかどうかを確認しています。

長い回答

guard let else

まず、

guard let xxx = yyy else { return }

yyyがOptionalで、yyyに値がなければreturnして値があればxxxにその値を入れる、という構文です。

例えば以下のように動きます。

func hoge(_ value: Int?) {
   gurad let v = value else { return }
   print(v)
}

hoge(10)  // prints 10
hoge(nil) // no prints

変数のキャプチャ

次に、関数による変数のキャプチャというものがあります。

var val = 0

func fuga() {
   val += 2   //  valをキャプチャ
}

print(val)  // prints 0.

fuga()
print(val)  // prints 2.

関数にキャプチャされた変数valは関数内で値を変更することが可能です。

Swiftでは関数とクロージャは同等のものですので以下でも同じです。

var val = 0

let fuga = { val += 2 }

インスタンスのオーナーシップ

最後にクラスのインスタンスのオーナーシップです。
クラスのインスタンスにはデータ領域がありますので、そのインスタンスを使わなくなった時にその領域が解放されなければメモリがひっ迫してしまいます。
しかし、使用中に開放されてしまうと、アプリケーションがクラッシュする事態になります。
このためSwiftではインスタンスにオーナーシップというものを設定しこれに対処しています。
原則として、使用中はオーナーを設定し使用が終わるとそれを外します。オーナーがすべてなくなった段階でインスタンスは解放されます。
これらは自動的に行われており、通常はプログラマは何も考える必要もありません。

ただし、循環参照という状態になるとインスタンスが解放されなくなるため、これを対処する必要があります。

class A {
  let b: B

  init(b: B) {
    self.b = b
    b.a = self
  }
}

class B {
   var a: A?
}

let a: A? = A(b: B())

この例は意味のあるコードではありませんが、循環参照の典型的な例です。
class Aのインスタンスはclass Bのインスタンスを、class Bのインスタンスはclass Aのインスタンスを参照しています。
そのため、上の例でa = nilとしてもa, bどちらのオーナーもなくならないため、どちらも解放されません。

この循環参照を回避するためにweakという修飾子が用意されています。
上の例ではclass Bの変数aweakとします。

class B {
   weak var a: A?
}

このweakという修飾子は「インスタンスは参照するがオーナーとしては登録するな。またインスタンスが解放されたとき値をnilにせよ。」という指示を与えます。
似た修飾子にunownedというものがあります。最後に少し説明をします。

こうすることでa = nilが実行されると、aのオーナーがすべてなくなりaが解放され、それによってbのオーナーもなくなるためbも解放されるようになります。

キャプチャによるオーナシップの獲得

まだまだ続きます。

つぎは今までできたことの複合技、キャプチャによるオーナシップの獲得です。

これは名前の通りです。先ほどのclass Aを使います。

var a: A? = A(b: B())

let f = { print(a) }  // インスタンスaをキャプチャ

a = nil // インスタンスaは解放されない

クロージャがインスタンスaをキャプチャした時、自動的にオーナーが設定されるため、a = nilを実行してもインスタンスaは解放されません。ただしこの場合は、fが使われなくなった時にfと一緒にaも解放されます。

これにも循環参照の問題が発生します。

class C {
  var f: (() -> Void)?
}

let c: C? = C()

c?.f = { print(c) }

c = nil  // c は解放されない

このようにインスタンスcが持つクロージャがcをキャプチャしてしまったため、循環参照が発生し、cが解放されなくなってしまいます。
この時もweak修飾子を使います。ただし、クロージャ自体をweak参照することができないため以下のように行います。

c?.f = { [weak c] in print(c) }

このweakcをキャプチャする時にオーナーを設定するなという指示になります。
この指定をすることでc = nilの時にインスタンスcが解放されるようになります。

とこらが、これでも問題が発生する場合があります。class Aを使います。

var a: A? = A(b: B())

let f = { [weak a] in print(a) }

a = nil

f()  // prints nil

aが解放された後にクロージャfを実行しようとすると、クロージャ内で使用しているaがすでに存在していないためnilになってしまいます。

キャプチャしたインスタンスの存在確認

この問題を解決するためには実行前にインスタンスaが存在することを確認する必要があります。
クロージャfを以下のようにします。

let f = { [weak a] in
    gurad let a = a else { return }
    print(a)
}

まず、gurad let a = a else { return }でインスタンスaの存在を確認し、その後それを使用するようにします。

冒頭のサンプルコードにあるgurad let self = self else { return }も同じことです。
[weak self]でキャプチャしたselfがクロージャ実行時に存在しているかどうかを確認しています。

おまけ

キャプチャ時のweak指定の副作用

すっかりだまされいるようですね。
書いてあることは正しいのですが、実はgurad let self = self else { return }についての説明が足りてません。

guard let xxx = yyy else { return }

yyyがOptionalで、yyyに値がなければreturnして値があればxxxにその値を入れる、という構文です。

と書きましたが、gurad let self = self else { return }の右のselfはOptional型ではないはずです。

ところが、

var a: A? = A(b: B())

let f = { [weak a] in print(a) }

これを

let a = A(b: B())

let f = { [weak a] in print(a) }

としたとしても、クロージャ内のaA型ではなくOptional<A>型になります。
これがキャプチャ時のweak指定の副作用です。
weakは「(略)解放されたとき値をnilにせよ」という指示なのでOptional型じゃないと無理なんです。
なので強制的にOptional型になります。

unowned

「循環参照を避けたいけど、weakだとOptionalになって面倒くさい! ここでは絶対インスタンスは存在してるのに!!!」
という時に使うのがunownedです。

weak修飾子は「インスタンスは参照するがオーナーとしては登録するな。またインスタンスが解放されたとき値をnilにせよ。」という指示でしたが、unownedは「インスタンスは参照するがオーナーとしては登録するな」だけの指示を与えます。
nilにする必要がないため、キャプチャ時に強制的にOptional型になることはありません。
アンラップを使うことなくそのまま利用できますので便利です。

ただし、当然のことながらunownedはオーナーを設定しないため利用しようとしたときにインスタンスが解放されてしまっていることがあります。
解放されたインスタンスにアクセスするとプログラムがクラッシュします。
unownedを使う場合はそのことに十分注意しましょう。

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

XcodeGen、Embedded Frameworkを導入しているOSSにwatchOSアプリを導入する

watchOS用アプリ作成を学ぶために、既にある程度設計や機能が確立しているOSSを題材にしてみました。
その際に既存の採用技術や設計にフィットさせていく際に詰まったところや困ったことをまとめてみました。
watchOSの開発は情報がかなり少ないので、同じく困った人の助けになれば幸いです。

前提

PokedexというOSSを題材にさせていただきました。
watchOSアプリを入れるにあたって、以下の技術を採用していることを前提に本記事を書いております。
- Embedded Framework
- Carthage
- XcodeGen
- Mint

watchOSアプリの導入がメインなので詳しくは割愛しますが、アプリ概要や採用している技術についての詳細はこちらをご覧ください。

また、今回はAppleWatch独立ではなく、Watch App for iOS Appのターゲットによって追加される、iPhoneと連携して動作するwatchOSアプリを前提としております。

WatchOSアプリ導入の手順

上記のような技術を採用しているPokedexに対して、以下の手順でwatchOSアプリを導入しました。

  1. 既存プロジェクトにまず手動でwatchOSアプリのターゲットを追加して、実機でビルドできることを確認する
  2. XcodeGen対応 - 手動で追加したwatchOSアプリのターゲット(WatchApp / WatchApp Extension)をXcodeGenでの導入に以降
  3. Carthage対応 - 導入ライブラリをwatchOSにも対応させる
  4. Embedded Frameworkの利用 - iOSと共有するソースをマルチモジュールwatchOS用のターゲットとしてマルチモジュール化してApple Watch側から利用できるようにする

Xcodegenで最初にプロジェクト作成した後、storyboardのリファレンスを繋ぎなおす

ターゲットの追加後project.ymlに加筆してビルドすると、以下のようなエラーを吐いて、ランタイムエラーになることがありました。

"NO". Couldn't instantiate class _TtC7Pokedex19InterfaceController

その際には、apple watch側で用意しているstoryboardのリファレンスが切れているので、Interface.storyboard -> Attributes Inspectorを選択し、Custom Classを再度指定してリファレンスを繋ぎ直すと解消されました。

Carthageのビルド対象となるプラットフォームにwatchOSを追加する

CocoaPodsやCarthageなどのパッケージマネージャーを利用して、watchOSのターゲットにもライブラリを導入したい場合、iOSに加えてwatchOSもプラットフォーム指定しなければなりません。

CocoaPods

// watchOSアプリ
target '[watchOS Extensionのターゲット]' do 
 platform :watchos, '2.0'
 pod 'Hoge'
end

Carthage

carthage bootstrap --platform iOS,watchOS

Carthage + Mint

今回のOSSではMintを使ってCarthageを導入しているのでこちら

mint run Carthage/Carthage carthage bootstrap --platform iOS,watchOS --cache-builds

生成されるCarthage/BuildwatchOSが追加されていればOK

watchOSApp ExtensionにFrameworkを関連づけた際に、実機デバッグでランタイムエラーになる

PokedexではEmbededd Frameworkを使用してappとは別にPresentation, Domain, DataStoreを用意してマルチモジュール化していました。
このうち例えば、DataStoreのソースをベースにwatchOS用にビルドして、AppleWatch側のターゲットと関連づけすれば、通信処理を共通化して利用できますし、依存関係もスッキリ保てます。

この際XcodeGenを採用しているので、project.ymlからソースコードベースでwatchOS用にFrameworkを関連づけなければなりませんが、僕の場合以下のランタイムエラーになりました。
WatchDataStoreはframework、watchOSApp Extensionはframeworkを関連づけるtargetです。

dependent dylib '@rpath/WatchDataStore.framework/WatchDataStore' not found for '[watchOSApp Extensionのpath]', tried but didn't find: '[WatchDataStoreのpath]' ...

FrameworkのEmbed設定が正しくないために発生しているエラーのようでした。また、シミュレーターだと通常通り動くので、実機で初めて発覚したバグでした。

これは、WatchOSAppExtensionとWatchDataStoreの関連づけにembed: trueのオプションを追加することで解消しました。
今回のwatchOSターゲットの追加では以下のようにproject.ymlに追加しました。

project.yml
targets:
  # iOS app
  Pokedex:
    type: application
    platform: iOS
    sources: [Pokedex]
    settings:
    # some setting
    dependencies:
    # some target
    - target: WatchOSApp
      codeSign: false
      embed: true      
  # iOS framework
  # some framework

  # watchOS
  WatchOSApp:
    type: application.watchapp2
    platform: watchOS
    sources: [WatchOSApp]
    settings:
      PRODUCT_NAME: Pokedex
      PRODUCT_BUNDLE_IDENTIFIER: pokedex.watchkitapp
      CODE_SIGN_STYLE: Automatic
    dependencies:
      - target: WatchOSAppExtension
  WatchOSAppExtension:
    type: watchkit2-extension
    platform: watchOS
    sources: [WatchOSApp Extension]
    settings:
      PRODUCT_NAME: WatchOSApp Extension
      PRODUCT_BUNDLE_IDENTIFIER: pokedex.watchkitapp.watchkitextension
      CODE_SIGN_STYLE: Automatic  
    dependencies:
      - target: WatchDataStore
        embed: true
  WatchDataStore:
    type: framework
    platform: watchOS
    sources: [DataStore]
    settings:
      PRODUCT_BUNDLE_IDENTIFIER: pokedex.WatchDataStore
      CODE_SIGN_STYLE: Automatic
    dependencies:
     - carthage: Alamofire
     - carthage: Nuke

その他

iOSアプリとwatchOSアプリでソースを共有する

iOSアプリと共有したいソースにwatchOSに対応していないライブラリを使用している場合、以下のように条件付きコンパイルを行う必要があります。
今回はSpotlight検索機能を提供するCoreSpotlightがwatchOSに対応していないので、watchOS用のターゲットに共有するソースコードから以下のようにOSごとの制御を行います。

#if os(iOS) || os(macOS)
import CoreSpotlight
#endif
// 以下watchOSでも使うライブラリ
import Foundation
import MobileCoreServices

複数のsimulatorを同時に起動しない

iPhoneと連動して動作するwatchOSのアプリを起動する場合、一度の起動でiPhoneとApple Watchの二つのシミュレーターを起動することになります。シミュレーターの端末を変えて確かめたくなった時に、iPhoneのシミュレータも異なるモデルに変わるので、試したい端末の倍のシミュレーターが動作しPCがフリーズしやすくなるので注意が必要です。

早めに実機で確認する

試験用watch端末を登録したプロビジョニングプロファイルを用意しなければいけないので後回しにしがちですが、watchターゲットを追加した段階で実機で確認するのが良いと思います。シミュレーターでは動くのに実機では起動できないなどシミュレーターではわからないことがたくさんある印象でした。
ただ、実機だとiPhone経由でビルドする分時間がかかるので、早期にまず実機でビルドできることを確認して、その後はシミュレーターで動作確認しつつ開発すると良いかと思います。

リンク

PokéAPIを利用してMVP+CleanArchitectureのiOSアプリを作ったので解説する

最後に

watchOSアプリの導入で詰まったところをまとめました。
情報が少なく、かなり試行錯誤した感があるので、もし間違えているところなどありましたら、そっと教えていただけますと幸いです。

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

UITableViewHeaderFooterViewのlabelにUITapGestureRecognizerを設定しても動作しない

UITableViewHeaderFooterViewで設定する場合

TopTableSectionHeaderView.swift
import UIKit
import RxSwift

class TopTableSectionHeaderView: UITableViewHeaderFooterView {

    @IBOutlet weak var categoryNameLabel: UILabel!

    @IBOutlet weak var categoryAddButton: UIButton!

    override init(reuseIdentifier: String?) {
        super.init(reuseIdentifier: reuseIdentifier)
        commonInit()
    }

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

    func commonInit(){
        let _view = UINib(nibName: "TopTableSectionHeaderView", bundle: nil)
            .instantiate(withOwner: self, options: nil)
            .first as! UIView

        _view.frame = bounds
        _view.backgroundColor = .green
        addSubview(_view)
        _view.autoresizingMask = [.flexibleWidth, .flexibleHeight]

        // うまくいく(printが実行される)
        let categoryAddButtonObservable = categoryAddButton.rx.tap.asObservable()
        categoryAddButtonObservable.subscribe(
            onNext: { [weak self] in
            print("rxButton")
            }
        )

        // うまくいかない(printが実行されない)
        let tapGesture = UITapGestureRecognizer()
        tapGesture.rx.event.bind(onNext: { recognizer in
            print("rxLabel")
        }).disposed(by: DisposeBag())
        _view.addGestureRecognizer(tapGesture)

        // うまくいかない(printが実行されない)
        let tapLabelGesture = UITapGestureRecognizer(target: self, action: #selector(labelDidTap(_:)))
        categoryNameLabel.addGestureRecognizer(tapLabelGesture)
    }

    @objc func labelDidTap(_ sender: UITapGestureRecognizer) {
        print("label")
    }
}

★補足

TopTableSectionHeaderView.swift
import UIKit

class TopTableSectionHeaderView: UITableViewHeaderFooterView {

    @IBOutlet weak var categoryNameLabel: UILabel!

    @IBOutlet weak var categoryAddButton: UIButton!

    override init(reuseIdentifier: String?) {
        super.init(reuseIdentifier: reuseIdentifier)
        commonInit()
    }

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

    func commonInit(){
        let _view = UINib(nibName: "TopTableSectionHeaderView", bundle: nil)
            .instantiate(withOwner: self, options: nil)
            .first as! UIView

        _view.frame = bounds
        _view.backgroundColor = .green
        addSubview(_view)
        _view.autoresizingMask = [.flexibleWidth, .flexibleHeight]

        // うまくいく(printが実行される)
        let tapViewGesture = UITapGestureRecognizer(target: self, action: #selector(viewDidTap(_:)))
        _view.addGestureRecognizer(tapViewGesture)

    }

    @objc func viewDidTap(_ sender: UITapGestureRecognizer) {
        print("view")
    }
}

ViewControlloerで設定する場合

TopViewController.swift
extension TopViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        let view = tableView.dequeueReusableHeaderFooterView(withIdentifier: "TopTableSectionHeaderView")
        if let v = view as? TopTableSectionHeaderView {
            v.setText("cate header!!!")
        }

        // うまくいく(printが実行される)
        let gesture = UITapGestureRecognizer(target: self, action: #selector(sectionHeaderDidTap(_:)))
        view?.addGestureRecognizer(gesture)
        view?.tag = section

        // うまくいく(printが実行される)
        let tapGesture = UITapGestureRecognizer()
        view?.addGestureRecognizer(tapGesture)
        tapGesture.rx.event.bind(onNext: { recognizer in
            print("rx")
        }).disposed(by: disposeBag)

        return view
    }

    @objc func sectionHeaderDidTap(_ sender: UITapGestureRecognizer) {
        print("tag \(sender.view?.tag)")
    }
}
TopViewController.swift
extension TopViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        let view = tableView.dequeueReusableHeaderFooterView(withIdentifier: "TopTableSectionHeaderView")
        if let v = view as? TopTableSectionHeaderView {
            v.setText("cate header!!!")

            // うまくいかない(printが実行されない)
            let tapGesture = UITapGestureRecognizer()
            v.categoryNameLabel.addGestureRecognizer(tapGesture)
            tapGesture.rx.event.bind(onNext: { recognizer in
                print("rx")
            }).disposed(by: disposeBag)

            // うまくいかない(printが実行されない)
            let gesture = UITapGestureRecognizer(target: self, action: #selector(sectionHeaderLabelDidTap(_:)))
            v.categoryNameLabel.addGestureRecognizer(gesture)
            v.categoryNameLabel.tag = section   
        }
        return view
    }

    @objc func sectionHeaderLabelDidTap(_ sender: UITapGestureRecognizer) {
        print("tag \(sender.view?.tag)")
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOS 14 における SwiftUI の新機能: @AppStorage, @SceneStorage, SwiftUI app

この記事では次の内容について説明します。
1. @AppStorage を使用して、SwiftUI アプリの UserDefaults を置き換える
2.@SceneStorage を使用して、1つのプログラムシーン(ウィンドウ)に固有の情報を保存する
3. SwiftUIApp Delegate 機能を実装する

@AppStorage

@AppStorage(wrappedValue: "Tokyo", "currentCity")
var currentCity

このコードは変数 currentCityUserDefaults に保存された値にリンクする

UserDefaultscurrentCity の値を求めようとし、もし求められれば、変数 currentCity は求められた値に相当する。

User Defaults がそれを求められなければ、デフォルトで wrappedValue を使用する。

変数 currentCity に変更が加えられた場合、その変更は保存される。

この SwiftUI プログラムによりテストが可能である:

struct ContentView: View {

    @AppStorage(wrappedValue: "Tokyo", "currentCity")
    var currentCity

    var body: some View {
        Text(currentCity).padding()
        TextField("", text: $currentCity)
    }

}

@SceneStorage

@SceneStorage@AppStorage とは違い、プログラムのシーンの状態を保存するために使います。 SceneStorage に保存されたデータは一つのシーン内にのみ在ります。(現在iOSは一つのアプリケーションで複数のシーンを持てるようになっているためです。)

struct textTabView: View {

    @SceneStorage("postContent")
    var postContent = "This is a draft..."

    var body: some View {
        TextEditor(text: $postContent)
    }

}

SwiftUI App アプリ用App Delegate

今年のWWDC 2020で、AppleはSwiftUIアプリを発表しました。SwiftUI appでは、App DelegateファイルとScene Delegateファイルがありません。このセクションでは、App DelegateのアップデートをSwiftUIアプリで受け取る方法をご説明します。

スクリーンショット 2020-07-14 午後2.24.36.png

バックグラウンド (background)、アクティブ (active)、 インアクティブ (inactive) 状態

scenePhase という環境変数を探し、その変数が変更された場合に通知されます。

struct ContentView: View {

    @Environment(\.scenePhase) private var scenePhase

    var body: some View {
        Text("Hello, world!").padding()

            .onChange(of: scenePhase) { phase in
                switch phase {
                    case .active:
                        print("active")
                    case .inactive:
                        print("inactive")
                    case .background:
                        print("background")
                    @unknown default:
                        print("unknown")
                }
            }

    }

}

ユニバーサルリンク / URLスキーム

URLスキームは、アプリを起動するために設定できるアプリ固有のURLです(app://)

https://developer.apple.com/documentation/uikit/inter-process_communication/allowing_apps_and_websites_to_link_to_your_content/defining_a_custom_url_scheme_for_your_app

ユニバーサルリンクは、ウェブサイトへアクセスした時にモバイルアプリを起動できます。

https://developer.apple.com/ios/universal-links/

これらのイベントを処理するには、.onOpenURL を使用します

@main
struct exploreSwiftUIApp: App {

    var body: some Scene {
        WindowGroup {
            ContentView()
                .onOpenURL { (url) in
                    print("Universal Link: \(url)")
                }
        }
    }

}

App Delegateファンクションを使用する

古い AppDelegate.swift ファンクションを使用したい場合は、アダプターを追加することができます。

@main
struct exploreSwiftUIApp: App {

    @UIApplicationDelegateAdaptor var delegate: AppDelegate

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }

}

class AppDelegate: NSObject, UIApplicationDelegate {

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        print("Did finish launch")
        return true
    }

}

他の種類の通知

以下を使って、SwiftUIで他の種類の通知を受け取ることができます

Text("Enter some text here")
    .onReceive(NotificationCenter.default.publisher(for: UIApplication.keyboardDidShowNotification)) { (output) in
        print("Keyboard is shown")
    }

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

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