- 投稿日:2019-10-12T22:35:31+09:00
[SwiftUI]NavigationLinkのアクセサリを非表示にするには
SwiftUIでは、画面遷移の方法としてNavigationLinkと呼ばれる機能が存在します。
これは、遷移後のViewと表示するViewを指定することでpush遷移を提供するviewです。
NavigationLinkは以下のように、Listの中で使うこともできます。struct ContentView: View { var body: some View { NavigationView { List { Text("cat") Text("dog") NavigationLink(destination: Text("detail")) { Text("fox") } } } } }しかし、Listの中でNavigationLinkを利用すると自動的に矢印のアクセサリ(UIKitで言う
UITableViewCell.AccessoryType.detailDisclosureButton
)が表示されます。これを非表示にするには、以下のようにStackの中でNavigationLinkと表示Viewを入れてNavigationLinkではEmptyViewを返すようにします。
struct ContentView: View { var body: some View { NavigationView { List { Text("cat") Text("dog") HStack { Text("fox") NavigationLink(destination: Text("detail")) { EmptyView() } } } } } }レイアウトによってはVStackとHStackを使い分ける必要があるかと思います。
- 投稿日:2019-10-12T17:32:44+09:00
SwiftUIとOCRライブラリ「SwiftyTesseract」を使って文字数カウントアプリを作ってみた
SwiftUI Tutorialsをとりあえずやったものの、以来触る機会がなかったので世の中に取り残される危機感を払拭するために、復習を兼ねて簡単な文字数カウントアプリを作ってみました。
以前から興味のあったOCRライブラリも一緒に試すことで一石二鳥で学習できたこともあり、せっかくなのでQiitaに残しておこうと思います。とてもシンプルなアプリなので、先に完成品のgifを貼っておきます。
ボタンには「撮影する」と表示されてますが、今回は省略してイメージライブラリの中から選択しています。対象読者、環境
- SwiftUI Tutorials 経験者
- Xcode 11 / Swift 5
さっそく実装
SwiftUI Tutorialsを経験していることを前提に話を進めるため、プロジェクトやファイルの作成等の基本的操作は省略します。まだの方は先に進めるか交互に見ながら読み進めることをおすすめします。
https://developer.apple.com/tutorials/swiftui最初のViewにButtonを追加
起動後の最初のViewに「撮影する」ボタンと「解析する」ボタンを追加します。
ファイル名はHomeView
としてますが、もちろんContentView
でも構いません。HomeView.swiftimport SwiftUI struct HomeView: View { var body: some View { VStack(alignment: .center, spacing: 10) { Button(action: { // }) { Text("撮影する") }.padding() .background(Color.green) .foregroundColor(.white) .cornerRadius(10) Button(action: { // OCR実行 }) { Text("解析する") }.padding() .background(Color.gray) .foregroundColor(.white) .cornerRadius(10) } } } struct HomeView_Previews: PreviewProvider { static var previews: some View { HomeView() } }画像を取得するためのImagePickerViewを追加
画像を取得するために
UIImagePickerViewController
を使います。Tutorialsの「Interfacing with UIKit」を参考にUIViewControllerRepresentable
を準拠させたViewクラスとUIImagePickerControllerDelegate
メソッドを利用するためのCoodinatorクラスを用意します。まずはCoodinatorに必要な
NSObject
とUIImagePickerViewControllerんい必要なUINavigationControllerDelegate
,UIImagePickerControllerDelegate
を準拠させたImagePickerCoordinator
クラスを作ります。ImagePickerView.swiftclass ImagePickerCoordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate { @Binding var isShown: Bool @Binding var uiImage: UIImage? init(isShown: Binding<Bool>, image: Binding<UIImage?>) { _isShown = isShown _uiImage = image } func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { uiImage = info[.originalImage] as? UIImage isShown = false } func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { isShown = false } }
isShown
は画像選択/キャンセル時に画面を閉じるため、uiImage
はイメージデータを参照するために@Binding
を付けてプロパティを定義しています。この設計であればクラスの責務が極端に肥大化することなく、View側はFatにならずに済みますね。ImagePickerView.swiftstruct ImagePickerView: UIViewControllerRepresentable { @Binding var isShown: Bool @Binding var uiImage: UIImage? func makeCoordinator() -> ImagePickerCoordinator { return ImagePickerCoordinator(isShown: $isShown, image: $uiImage) } func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController { let picker = UIImagePickerController() picker.delegate = context.coordinator return picker } func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) { } }こちらは先ほどの
ImagePickerCoordinator
を利用して間接的にデリゲートメソッドを呼べるようにしています。HomeViewからImagePickerを呼び出し
sheet
メソッドを追加して「撮影する」ボタン押下時にImagePickerView
へ遷移させます。画像選択後はボタンの上に画像を表示させます。
また、画像が無いときは「解析ボタン」を無効にしたいので、真偽値を渡して制御するようにしました。HomeView.swiftimport SwiftUI struct HomeView: View { @State private var showImagePicker: Bool = false @State private var uiImage: UIImage? = nil private var hasImage: Bool { return uiImage != nil } var body: some View { VStack(alignment: .center, spacing: 10) { if hasImage { Image(uiImage: uiImage!) .resizable() .scaledToFit() } Button(action: { self.showImagePicker = true }) { Text("撮影する") }.padding() .background(Color.green) .foregroundColor(.white) .cornerRadius(10) Button(action: { // OCRを実行 }) { Text("解析する") }.padding() .background(hasImage ? Color.orange : Color.gray) .foregroundColor(.white) .cornerRadius(10) .disabled(!hasImage) }.sheet(isPresented: $showImagePicker) { ImagePickerView(isShown: self.$showImagePicker, uiImage: self.$uiImage) } } } struct HomeView_Previews: PreviewProvider { static var previews: some View { HomeView() } }「SwiftyTesseract」をプロジェクトへ追加
今回はCocoaPodsでインストールしました。もちろんライブラリ管理ツールは、SwiftUIに関わらずこれまで通り使えます。
但し、ライブラリ側がSwift 5に対応している必要があるのでそこは注意が必要です。「SwiftyTesseract」はリリースバージョン自体はまだSwift 4.2まででしたが、Swift 5に対応したブランチが存在したためそちらを利用しました。
pod 'SwiftyTesseract', git: 'https://github.com/SwiftyTesseract/SwiftyTesseract.git', branch: '3.0.0'学習データをプロジェクトへ追加
SwiftyTesseractでは、Tesseract用にあらかじめ用意されている学習データをそのまま利用することができます。
今回は日本語を取り扱う文字数カウントをしたいので日本語学習データを利用します。tesseract-ocr/tessdata_fast から速度重視の学習データをダウンロードします。 ?こちら
プロジェクトの以下のようにtessdataフォルダを作成し、ダウンロードしたファイルを追加します。これだけで自動で学習データを読み込んでくれます?
「撮影する」ボタン押下時にOCRの実行
SwiftyTesseractをプロジェクトに追加できたらimportしてコードを書いていきます。
取得した文字列は空行や空文字が入っており、純粋にカウントすると文字数以上の数値になってしまうのでトリミングしました。HomeView.swiftimport SwiftUI import SwiftyTesseract struct HomeView: View { ... @State private var characterCount: Int? = nil var body: some View { VStack(alignment: .center, spacing: 10) { ... Button(action: { let swiftyTesseract = SwiftyTesseract(language: .japanese) let result = swiftyTesseract.performOCR(on: self.uiImage!) switch result { case .success(let string): print(string) let trimString = string.components(separatedBy: .whitespacesAndNewlines).joined() print(trimString) self.characterCount = trimString.count case .failure: break // Handle the error } }) { Text("解析する") }.padding() .background(hasImage ? Color.orange : Color.gray) .foregroundColor(.white) .cornerRadius(10) .disabled(!hasImage) ...実行結果をTextに反映
最後に「解析する」ボタンの下に以下のコードを追加して文字数を表示するようにして終わりです。
if characterCount != nil { Text("文字数: \(characterCount!)") }あとがき
今回の内容もSwift Tutorialsと同様のレベルではあるものの、事前に用意されていないものを作ることでより実になったように感じます。また思っていたよりもコードが少なく、シンプルに書けたのでSwift UIにより魅了されました。また、Swift UIとWi-Fi 実機デバッグの開発体験は非常に良くて、動作確認すでも待ちやモヤモヤが随分解消されました。
SwiftyTesseractについては、最初の画像以外にもいろいろ試したところ思ったよりも精度が高かったのですが、やはり長文や手書きだと精度が落ちてしまうため、次の学習としてML Kit for Firebaseを試そうかと思っています。
Swift UIを使うとクライアントの準備コストが下がるため、バックエンド側の試作に注力できるのが良いですね。
参考
- 投稿日:2019-10-12T16:37:34+09:00
【Swift】 WKWebViewで表示したtitleを対応するタブボタンに表示させる。
こちらの記事で書いている、オリジナルタブブラウザアプリの、追加機能です。
タブブラウザでは、タブボタンにそのタブで開いているWebページのtitleが表示されるのが一般的だと思いますが、自作アプリでそれを実現する方法を確認しました。開発環境
端末:MacBook Pro/MacOS 10.14.5(Mojave)
Xcode:10.2.1
Swift:5やったこと(ポイント)
①WKWebViewで開いたWebページのtitle(titleタグの情報)を、タブボタンに表示する。
実装
画面イメージ
開いているWebページに対応するtitleがタブボタンに表示されています。
アプリ自体のUIデザインがどうかしちゃってるのは見逃して下さい。。
yahoo!japan
bing ソースコード
要点だけ。
webView(_:didFinish:)を追加し、ページ読み込み完了時の処理を記述します。
ViewController.swiftfunc webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { let targetTag = webView.tag - 10000 let targetBtn = view.viewWithTag(targetTag) as! UIButton let title = webView.title targetBtn.setTitle(title, for: .normal) }自分がどれくらい独特な書き方しちゃってるか自覚がないですが、Webページ(WKWebView)とタブボタン(UIButton)の紐付けを、tagの値を使って制御しているので上記コードのような書き方になっています。
以上です。
- 投稿日:2019-10-12T16:32:33+09:00
SwiftUIで何気なく使っている some を調べてみる
モチベーション
var body: some View { ... }このコードはもう100回は書いてますが、
some
は Swift5.1で追加された Opaque Result Types の Proposalに準じたものてぐらいの認識で、Proposalの中身をちゃんと理解できていなかった。
SwiftUI簡潔に書けるすげー!! アニメーションめっちゃ簡単に書けるやん!!て感動している今だから、あえて立ち止まって技術について調べたいと思う。自分の理解を深めるために、記事にしております。
さて、Opaque Result Types何?
Opaqueて、
不透明な、明確でない
て意味なんで、不透明な戻りの型
つまりGenericsみたいなものかと予想はしていた。実際に、SwiftUIでリストを書こうとすると
var body: some View { List { Text("1行目") Text("2行目") } }レイヤーを重ねたようなデザインにしようとすると、以下のように書ける
var body: some View { ZStack { Image("xxxx") Text("Hello world!") } }戻り値は変わらず常に
some View
である。
画面によってデザインが全く異なるものを同一の戻り値として扱っていることになる。そもそも記事をまとめようとしたきっかけは、Swift 5.1 に導入される Opaque Result Type とは何かにかかれています、『リバースジェネリクス』について記述を見たためです。
protocol Animal { func say() } class Dog: Animal { func say() { print("bark bark") } } class Bird: Animal { func say() { print("sing sing") } } // ジェネリクス func say<A: Animal>(_ animal: A) { animal.say() } // リバースジェネリクス ←あくまで概念でコンパイルは通りません func makeAnimal() -> <A: Animal> A { return Dog() } say(Dog()) // bark bark makeAnimal().say() // bark bark
- (ジェネリックス) say の 利用者 が A の具体的な型を定め、 say の 実装者は抽象的な A に対してコードを書く。
- (リバースジェネリックス) makeAnimal の 実装者 が A の具体的な型を定め、 makeAnimal の 利用者 は抽象的な A に対してコードを書く。
Swift 5.1 に導入される Opaque Result Type とは何かでは、
とらえどころのない Opaque Result Type ですが、『リバースジェネリクス』という概念を導入すると、
Opaque Result Type は『リバースジェネリクス』のシンタックスシュガー
である とシンプルに考えることができます。 つまり、次の二つが等価だということです。シンタックスシュガー: 別の構文や記法で記述できるようにしたもの。
// 『リバースジェネリクス』 func makeAnimal() -> <A: Animal> A { return Dog() } // Opaque Result Type func makeAnimal() -> some Animal { return Dog() }「makeAnimal の 実装者 が A の具体的な型を定め、 makeAnimal の 利用者 は抽象的な A に対してコードを書く」
をSwiftUIに置き換えると以下のように言えると思います。makeAnimal の 実装者 が A の具体的な型を定め →
var body: some View
の中身を書いている我々アプリ開発者が具体的なレイアウトAを定めmakeAnimal の 利用者 は抽象的な A に対してコードを書く → SwiftUIの開発元であるAppleは抽象的な A に対してアプリとして表示できる仕組みを提供する
文章をつなげると、
「var body: some View { ... }
の中身を書いている我々アプリ開発者が 具体的なレイアウトAを定め、SwiftUIの開発元であるAppleは抽象的な A に対してアプリとして表示できる仕組みを提供する」先に示したリバースジェネリックスの例を
some
を使って書き換えて実行してみます。func makeAnimal1() -> some Animal { return Dog() } func makeAnimal2() -> some Animal { return Bird() } makeAnimal1().say() // bark bark makeAnimal2().say() // sing singどちらも期待通りの結果を出力してくれます。
またAppleのDocumentにも
Returning an Opaque Type
You can think of an opaque type like being the reverse of a generic type.Opaque Type はGenericsの逆ととらえられるということですね。
まとめ
some
をつけることで 様々なレイアウトの画面を抽象的に扱うことができるようになる。some
(Opaue Type)はGenericsの逆なのね。ネットで
Opaque Result Types
でググったが実用的な例は見つからなかった。せっかく5.1で実装されたものなので有効に活用してみたいところ。参考
- 投稿日:2019-10-12T14:31:30+09:00
【Swift】Swiftのメモリ管理を内部の仕組みから学ぶ
普段コードを書いている時に
Swiftが内部で
どのようにオブジェクトを管理しているのかについて
考えることはあまり多くないかもしれません。しかし
非同期処理を扱う場合などDispatchQueue.main.async { [weak self] in ... }などのように
weak
といった
キーワードを使用することは多くあると思います。これは「弱参照」と呼ばれ
直接の参照(強参照)を持たないように
Swiftに内部に指示をして
循環参照を起こさないための仕組みです。こういった適切なメモリ管理を行わないと
メモリが解放されないことでランダムにクラッシュが起こるなど
原因がわかりづらい不具合を発生させる可能性があります。そこで今回は
weak
などを使用することで
Swiftが何をしているのかを
内部の仕組みから見ていくことで
weak
やunowned
の使い方や
オブジェクトのライフサイクルについて
学んでみたいと思います。メモリの3つ仮想的な領域
メモリ自体はただのバイトの配列ですが
プログラミングという観点ですと
3つの領域に分けることがよくあります。
- スタック領域 全てのローカル変数を保管している場所です。
- グローバルデータ 静的な変数や定数や型のメタ情報を保管している場所です。
- ヒープ領域 オブジェクト※を保管している場所です。 ※ 実行時にメモリを割り当てられ ある時点で解放される「寿命」を持つものを指します。 Swiftでは主に参照型(reference type)を指します。
ARC(Automatic Reference Counting)
メモリ管理には「オーナーシップ(所有権の帰属)」という概念が大切になります。
これはあるオブジェクトを誰が解放する責務を持っているかということを意味します。詳細に関しては下記のOwnershipManifestに記載されています。
https://github.com/apple/swift/blob/master/docs/OwnershipManifesto.mdこのオーナーシップを管理する仕組みとして
SwiftはARCを使用しています。ARCはオブジェクトの参照されている数を保持しておき
カウントがゼロになるとメモリから自動で解放される仕組みです。2つのレベルの参照(強参照と弱参照)
Swiftでは参照に2つのレベルがあります。
それが冒頭でも登場した
強参照(strong reference)
と
弱参照(weak reference)
です。さらに
弱参照の派生として非参照(unowned reference)があります。強参照(strong reference)
Swiftでは
強参照がある限り
オブジェクトは生存し続けることができ
逆になくなるとメモリから解放されます。SwiftではJavaのガベージコレクションのように
自動でメモリを解放する仕組みは持っていないため
循環参照を引き起こす可能性があります。これは例えば
オブジェクトAとオブジェクトBが
お互いにお互いのオブジェクトへの参照を持っている状態です。これを防ぐためにも
weak
などの記述が必要になってきます。弱参照(weak reference)
弱参照を使用することで
循環参照を断ち切ることができますが
弱参照を持っていたとしても
強参照がなくなればオブジェクトはメモリから解放されてしまいます。その場合
参照しているオブジェクトを使おうとしてもnil
になっています。そのため
weak
を使用する時はOptional
で扱われます。非参照(unowned reference)
弱参照とほぼ同じですが
参照しているオブジェクトを使おうとすると
assertionエラーとなってプログラムはクラッシュします。
unowned
は参照がなくならないと想定しているのに
予期せず参照が解放されてしまっている不具合を発見するのに役立ちます。
unowned
を使用する基準としては
参照するオブジェクトと参照されるオブジェクトの寿命が同じような時です。例えばクラスの中で
lazy
をつけた変数を定義する場合にclass ViewController: UIViewController { lazy var label: UILabel = { [unowned self] in self.someSetup() ... }() }これはViewControllerクラスの変数labelが
self(ここではViewController)を参照しています。これはクラスオブジェクトが解放されるタイミングで
label変数も解放されるので
selfの参照がなくなることはありません。参照については下記のドキュメントに詳細が記載されています。
https://github.com/apple/swift/blob/master/docs/weak.rstSwift Runtime
ARCはSwift Runtimeというライブラリで実装されています。
他にもSwift Runtimeでは
実行時にジェネリクスやプロトコルを具体的な型に解決する
などの重要な機能を有しています。https://github.com/apple/swift/blob/master/docs/Runtime.md
全てのオブジェクトは
HeapObject
というstruct
で表現されます。HeapObjectはオブジェクトの型のメタ情報と参照カウント(RefCount)を持っています。
RefCountにはstrong
とweak
とunowned
用の3種類があります。
https://github.com/apple/swift/blob/master/stdlib/public/SwiftShims/RefCount.hSwiftのコンパイラはSIL生成段階(SIL Generation)※で
swift_retain()
swift_release()
というメソッドを適切な場所に差込みます。これによって
HeapObject
の作成や解放がされます。※
SIL生成段階(SIL Generation)はSwiftのコンパイル時の一つのフェーズです。
下記のドキュメントに詳細が記載されています。
https://swift.org/compiler-stdlib/#compiler-architectureSide Table
全てのオブジェクトは
弱参照(weak reference)用のRefCountを持っているものの
多くのオブジェクトで弱参照を持っていません。
そこで弱参照用のRefCountにメモリを割り当てても無駄になることが多いため
弱参照の情報はSide Table※という別の場所に保管され
本当に必要になった時にメモリが割り当てられるようになっています。※
正式にはHeapObjectSideTableEntry
です。
内部ではオブジェクトのポインタとRefCountを持っています。
https://github.com/apple/swift/blob/master/stdlib/public/SwiftShims/RefCount.h#L1199弱参照が示すメモリアドレスは参照したいオブジェクトではなく
このSide Tableを示しています。これによって
無駄なメモリが必要なくなることに加え
直接オブジェクトを参照していないため
オブジェクトの解放と弱参照が参照するタイミングが競合することなく
弱参照を取り除くことができます。オブジェクトのライフサイクル
下記のコメントを参考にすると
https://github.com/apple/swift/blob/master/stdlib/public/SwiftShims/RefCount.h#L112Swiftのオブジェクトは3つの参照の保持の仕方によって
状態を5つに分けることができます。
- LIVE
- DEINITING
- DEINITED
- FREED
- DEAD
簡単に図にすると下記のように状態が変化していきます。
次にそれぞれの状態について
見ていきます。各状態で弱参照(Side Table)があるかないかで
挙動や次に遷移する状態が変化していきます。LIVE without side table
オブジェクト
生存している。
参照カウント
強参照1 非参照1 弱参照1で初期化される。
Side Tableと弱参照カウントのメモリ割り当て
なし 。
強参照変数の操作
正常に機能する
非参照変数の操作
正常に機能する。
弱参照変数を使用した時の挙動
起こり得ない。
弱参照変数へのオブジェクトの代入
Side Tableを追加する。
LIVE with Side Table状態になる。次の状態への遷移
参照がゼロになった時に
deinitが呼ばれDEINITINGの状態になる。LIVE with side table
弱参照変数の操作
正常に機能する。
それ以外は
LIVE without side tableと同じ。DEINITING without side table
オブジェクト
deinit()
を実行中。強参照変数の操作
何も起こらない。
非参照変数を使用した時の挙動
swift_abortRetainUnowned()
で処理が中断されます。
https://github.com/apple/swift/blob/ebcbaca9681816b9ebaa7ba31ef97729e707db93/include/swift/Runtime/Debug.h#L122https://github.com/apple/swift/blob/master/stdlib/public/runtime/Errors.cpp#L451
非参照変数へオブジェクトを代入した時の挙動
正常に機能する。
弱参照変数を使用した時の挙動
起こり得ない。
弱参照変数へオブジェクトを代入した時の挙動
nilが代入される
次の状態への遷移
参照がゼロになった時の挙動
deinit()
が完了し
swift_deallocObject()
というメソッドが呼ばれる。
canBeFreedNow()
で弱参照または非参照があるかどうかをチェックする。
canBeFreedNow
がtrueの場合
オブジェクトは解放されてDEINITEDの状態になる。DEINITING with side table
弱参照変数を使用した時の挙動
nilを返却する。
弱参照変数へオブジェクトを代入した時の挙動
nilが代入される。
canBeFreedNow()
は常にfalseになり
そのままDEAD状態にはならない。その他はDEINITING without side tableと同じ。
DEINITED without side table
オブジェクト
deinit()
は完了しているが
非参照は存在している。強参照変数の操作
起こり得ない。
非参照変数を使用した時の挙動
swift_abortRetainUnowned()
の中でロードを停止している。非参照変数へオブジェクトを代入した時の挙動
起こり得ない。
弱参照変数の操作
起こり得ない。
次の状態への遷移
非参照カウントがゼロになった時
オブジェクトは解放されてDEAD状態になる。DEINITED with side table
弱参照変数を使用した時の挙動
nilが返却される。
弱参照変数へオブジェクトを代入した時の挙動
起こり得ない。
次の状態への遷移
非参照カウントがゼロになった時
オブジェクトは解放されて弱参照カウントが減り
オブジェクトはFREED状態になる。他はDEINITED without side tableと同じ
FREED without side table
起こり得ない状態。
FREED with side table
オブジェクト
オブジェクトは解放されているが
弱参照がSide Tableに残っている状態。強参照の操作
起こり得ない。
非参照の操作
起こり得ない。
弱参照変数を使用した時の挙動
nilが返却される。
弱参照変数へオブジェクトを代入した時の挙動
起こり得ない。
次の状態への遷移
弱参照カウントがゼロになった時
Side Tableオブジェクトは解放され
オブジェクトはDEAD状態になる。DEAD
オブジェクトもSide Tableもなくなっている。
まとめ
Swiftの内部の仕組みから
メモリ管理について見ていきました。普段はあまり意識していませんでしたが
こういう知識を知っていることで
原因がわからない不具合などの解決にも
役に立つことがあるかもしれません。また
XcodeにはMemoryDebuggerもあり
そのグラフを理解するのに役に立つかもしれません?もし何か間違いなどございましたら
ご指摘頂けましたらうれしいです??♂️参考記事
https://www.vadimbulavin.com/swift-memory-management-arc-strong-weak-and-unowned/
- 投稿日:2019-10-12T14:07:38+09:00
UIVisualEffectViewを使ってUIVibrancyEffectだけを表示する
はじめに
UIVisualEffectViewとは
UIVisualEffectViewは背後にあるViewにエフェクトをかけてくれるViewです。
UIVisualEffectViewにはBlurEffectとVibrancyEffectがあります。
Blur:背後のViewにエフェクトをかけることによって、磨りガラス様な感じになります
Vibrancy:UIBlurEffectのViewの上に置くことによって。曇りガラスの様になります。
使うときはUIVisualEffectViewにaddSubviewしないで、UIVisualEffectView.contentViewに追加します
iPhoneでパスコード入力画面や、コントロールセンターで使われており、以下のパスコード入力画面では、背景がBlurEffectでNumberButtonの背景がVibrancyEffectになっています。
今回はUIBlurEffectは表示せず、UIVibrancyEffectだけ表示してみよう思います。
やってみる
まずはImageViewの上にVisual Effect View with Blur and Vibrancyを置いてみます。
そうすると磨りガラス効果のViewと曇りガラス効果のViewが一気に展開されます。
そこにTextなどを入れると、この様な曇りガラスのテキストになります
ここからUIVibrancyEffectがかかったTextだけを表示していきたいと思います
方法としてはBlurEffectViewにmaskをかけていきます、var mask: UIView? { get set }maskはUIView型なので、UIImageViewやUIViewなど色々な物をUIVibrancyEffectだけを表示ことが可能です
class CatalinaViewController: UIViewController { @IBOutlet weak var text: UILabel! @IBOutlet weak var blurEffectView: UIVisualEffectView! override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. self.blurEffectView.mask = text } }アプリを実行すると・・・・・・・・・できました!!
参考リンク
- 投稿日:2019-10-12T12:43:46+09:00
iOS13 での問題
iOS 13 がリリースされ、既存のアプリで問題が発生しているようなので、気づいたことをメモしたいと思います。
iOS 13 での問題
- iPhone の電話履歴からVoIPアプリで発信できない
- iPad OS 13 で、UIDocumentInteractionController で開く共有の画面が正しく表示されない
iPhone の電話履歴からVoIPアプリで発信できない
iPhone の電話帳や履歴から発信した場合、VoIP アプリの application(_:continue:restorationHandler:) が呼び出されます。
音声通話であれば INStartAudioCallIntent、ビデオ通話であれば INStartVideoCallIntent が userActivity.interaction.intent に渡されます。
いえ、iOS 12 までは渡されていました・・・
iOS 13 では INStartCallIntent が渡されてくるようになりました。
音声かビデオかを判断するのは Intent の種類ではなく、INStartCallIntent.callCapability で判断するようになります。
この渡される Intent が変更された件、困ったことに xcoode 11(iOS13 SDK) でビルドしたものではなく、xcode 10.x でビルドしたアプリも同じです・・・
VoIP アプリでは application(_:continue:restorationHandler:) に渡されてくる Intent から contacts を取得します。
Swift の場合、Intent をキャストした際、型が合わないと nil となりますが、Objective-C の場合、キャストしてもエラーにならず、存在しないメンバにアクセスした際に初めてエラーとなります。
したがって、contacts は INStartAudioCallIntent も INStartCallIntent にも含まれているため、Objective-C で書かれたプログラムでは書き方次第では iOS 13 でも動いているようです。iPad OS 13 で、UIDocumentInteractionController で開く共有の画面が正しく表示されない
ファイルや写真などを他のアプリに共有する際に使用する UIDocumentInteractionController ですが、よくある書き方として以下のようなコードが散見されます。
uiDocumentInteractionController.presentOpenInMenu(from: view.frame, in: view, animated: true)UITabBarController に追加した ViewController の中でこのコードを実行すると、iPad OS 13 では、共有先選択画面が Tab のところに小さくなって表示されます。
View の下端に表示しようとするが、Tab の高さしかないため縦が小さくなっているのではないかと思われます。
よって、iPad OS 13 対応するためには、from と in に上の方にあるコントロールを指定する工夫が必要なようです。(バグとして iOS 側で修正が入るかもしれませんが・・・)
しかし、from と in を上方のコントロールに指定して、xcode 10.x でビルドしたところ、iPhone (iOS 13) では共有先選択画面が全画面表示してしまいました。。。
まぁ、見えないよりぜんぜん良いのですけどね。。。
ちなみに同じコードを xcode 11 でビルドすると、いつものように下半分くらいに共有先選択画面が表示されます。。。Apple さん・・・・・
- 投稿日:2019-10-12T11:15:37+09:00
SwiftUI Font指定
- 投稿日:2019-10-12T11:01:45+09:00
SwiftUIとUIKitが共存するアプリにおける色指定の注意点 (sRGB vs Generic RBG)
モチベーション
UIKitを併用していて、Color.gray と、 UIColor.gray で指定した部分で色が異なることに気づきました。
SwiftUIでは、色は、Color
UIKitでは、UIColor
と別の型である。コンソールにprint文出力して確認してもやはり値が異なる。
Gray 値 Color 0x600002ea6b20 UIColor 0x6000030b1170 そもそも何故、SwiftUIだけではなく、UIKitを併用しないといけなかった理由は後半に書きます。
ColorのデフォルトはsRGB
Colorの宣言部分をみるとデフォルト引数が sRGBになっていることがわかる。
extension Color { public enum RGBColorSpace { case sRGB case sRGBLinear case displayP3 ... public init(_ colorSpace: Color.RGBColorSpace = .sRGB, red: Double, green: Double, blue: Double, opacity: Double = 1) } }SwiftUIの色のデフォルトは sRGB指定である。そしてUIColorで指定できる、Generic RGBが選択肢にない。
UIColorで使い方によってRGBの種類が異なるのか?
使用方法 デフォルト 例 コード sRGB label.backgroundColor = UIColor(red: 245/255.0, green: 84/255.0, blue: 45/255.0, alpha: 1.0) Color Literal sRGB AssetCatalogのColorSet sRGB すべて、sRGBがデフォルトであることがわかる。
Appleの公式を読むと
https://developer.apple.com/documentation/uikit/uicolorFor apps linked against the iOS 9 SDK and earlier, or running on iOS 9 and earlier, colors use one of two color spaces: - Device-Dependent Gray - Device-Dependent RGBUIColor.gray は iOS9以前から存在したからこれを Generic RGBから sRGBに 色合いを変えることはできなくて取り残されていると考えられる。
みんな大好き16進数色指定extension
今回のようにデフォルトの
gray
を使用せずに、 個々で色を指定すると思うが、 せっかくなので、 16進数指定できるものを用意した。 Apple 的には Dark Mode対応のため、AssetCatalog で色指定してほしいのかな?なので使いみちはないかもしれないが一応書いておく。// MARK: - Helper extension UIColor { convenience init(hex: Int, alpha: CGFloat = 1) { let components = ( R: CGFloat((hex >> 16) & 0xff) / 255, G: CGFloat((hex >> 08) & 0xff) / 255, B: CGFloat((hex >> 00) & 0xff) / 255 ) self.init(red: components.R, green: components.G, blue: components.B, alpha: alpha) } } extension Color { init(hex: Int, alpha: Double = 1) { let components = ( R: Double((hex >> 16) & 0xff) / 255, G: Double((hex >> 08) & 0xff) / 255, B: Double((hex >> 00) & 0xff) / 255 ) self.init( .sRGB, red: components.R, green: components.G, blue: components.B, opacity: alpha ) } }使い方
Color(hex: 0xdddddd) UIColor(hex: 0xdddddd)ちょっと補足、
UIColorは32bitも使われる時代に作られたものなので、alphaの引数が Doubleではなく、 CGFloatのエイリアスで32bitか64bitかでFloatなのかDoubleなのかを切り替えているが、Colorからは全面的に64bitなので気持ちよくDoubleに振り切っている。(さすがAppleだ)/// The native type used to store the CGFloat, which is Float on /// 32-bit architectures and Double on 64-bit architectures. public typealias NativeType = Doubleそれとalphaを opacity という単語に変えている。
補足
そもそもなぜアプリ内で UIColorを使おうと思ったかというと
以下の図を見てもらうとわかるように、Listを引っ張った時に、背景が白色で表示されてしまうのを対応するためinitの中で、UITableViewのbackgroundColorを指定するためである。これだけのために、UIKitを指定するのは正直イケていないので、Appleが指定できるIFを用意してくれることを望む(もしSwiftUIで指定する方法をご存じの方がいましたら教えて下さい)
init() { UITableView.appearance().backgroundColor = UIColor(hex: 0xdddddd) } var body: some View { List(consoleLogs, id: \.self) { value in Text(value) .lineLimit(Int.max) .fixedSize(horizontal: false, vertical: true) } } }まとめ
ほとんどのアプリではGeneric RGBを使わずにsRGBを使っていて、UIColor.gray みたいなデフォルトの色を使うことはないと思うので実害はないだろう。
参考
- 投稿日:2019-10-12T10:05:09+09:00
[Swift]2001年1月1日からの日数をカウントするプログラム DateComponents
DateComponentsを利用していますが計算の本質ではないです。
extension DateComponents { //need year, month, day func dateCountFrom20010101() -> Int? { let y = year! - 2001 let m = month! - 1 let d = day! - 1 var count = y * 365 + y/4 - y/100 + y/400 if (year! % 4 == 0 && year! % 100 != 0) || year! % 400 == 0 { //uru doshi let tmp = [0,31,60,91,121,152,182,213,244,274,305,335] count += tmp[m] + d } else { let tmp = [0,31,59,90,120,151,181,212,243,273,304,334] count += tmp[m] + d } return count } }以下はテストコード。
20001/01/01のDateをとり、i日進めたものをnewDateとします。
newDateからnewCompを取り出して、dateCountFrom20010101の値とiを比較します。
10000年だと処理に1分くらいかかります(かかりました。)let a = 365 * 10000 let b = DateComponents(calendar: nil, timeZone: nil, era: nil, year: 2001, month: 1, day: 1, hour: 12, minute: nil, second: nil, nanosecond: nil, weekday: nil, weekdayOrdinal: nil, quarter: nil, weekOfMonth: nil, weekOfYear: nil, yearForWeekOfYear: nil) let date20010101noon = Calendar(identifier: .gregorian).date(from: b)! let daysecond = 24 * 60 * 60 print("start--------------------") for i in 0..<a { let newDate = Date(timeInterval: TimeInterval(daysecond * i), since: date20010101noon) let newComp = Calendar(identifier: .gregorian).dateComponents([.year,.month,.day], from: newDate) let j = newComp.dateCountFrom20010101() if i != j { print("failed:\(j)") } } print("end-------------------")
- 投稿日:2019-10-12T04:31:02+09:00
[SwiftUI]生成されたUIViewを直接操作する
これは実験的な話です。
次のようなRootViewで構築したビューはUIKitのビューヒエラルキーにUITableViewが含まれているはずです。
struct Message: Identifiable { let id: Int } struct ContentView: View { @State var messages: [Message] = (0..<100).map({ Message(id: $0) }) var body: some View { List { ForEach(messages, id: \.id) { Text("\($0.id)") } } } }このUITableViewを直接参照して操作出来るのか試してみます。
まずはビューヒエラルキをダンプOptional(<UIWindow: 0x7fe6e5b25710; frame = (0 0; 414 896); gestureRecognizers = <NSArray: 0x60000286df80>; layer = <UIWindowLayer: 0x600002633c40>>) Optional(Swift.Unmanaged<Swift.AnyObject>(_value: <UITransitionView: 0x7fe6e371abd0; frame = (0 0; 414 896); autoresize = W+H; layer = <CALayer: 0x60000260a400>> | <UIDropShadowView: 0x7fe6e371ba30; frame = (0 0; 414 896); clipsToBounds = YES; autoresize = W+H; layer = <CALayer: 0x60000260a920>> | | <_TtGC7SwiftUI14_UIHostingViewV6Scroll11ContentView_: 0x7fe6e370c4a0; frame = (0 0; 414 896); autoresize = W+H; gestureRecognizers = <NSArray: 0x60000284ccf0>; layer = <CALayer: 0x600002637e20>> | | | <_TtGC7SwiftUI16PlatformViewHostGVS_P13$7fff2c6cf80432PlatformViewRepresentableAdaptorGVS_P13$7fff2c69c16c9_ListCoreGVS_20SystemListDataSourceOs5Never_GOS_19SelectionManagerBoxS4_____: 0x7fe6e5b2b220; frame = (0 0; 414 896); anchorPoint = (0, 0); tintColor = UIExtendedSRGBColorSpace 0 0.478431 1 1; layer = <CALayer: 0x600002606220>> | | | | <_TtC7SwiftUIP33_BFB370BA5F1BADDC9D83021565761A4925UpdateCoalescingTableView: 0x7fe6e5029c00; baseClass = UITableView; frame = (0 0; 414 896); clipsToBounds = YES; gestureRecognizers = <NSArray: 0x600002841470>; layer = <CALayer: 0x600002604220>; contentOffset: {0, -44}; contentSize: {414, 4406.666666666667}; adjustedContentInset: {44, 0, 34, 0}; dataSource: <_TtGC7SwiftUIP13$7fff2c69bad819ListCoreCoordinatorGVS_20SystemListDataSourceOs5Never_GOS_19SelectionManagerBoxS2___: 0x7fe6e3605c80>> | | | | | <SwiftUI.ListCoreCellHost: 0x7fe6e3731c20; baseClass = UITableViewCell; frame = (0 1330; 414 44.3333); autoresize = W; layer = <CALayer: 0x60000267b620>> | | | | | | <UITableViewCellContentView: 0x7fe6e3731fe0; frame = (0 0; 414 44.3333); gestureRecognizers = <NSArray: 0x600002887450>; layer = <CALayer: 0x60000267b960>> | | | | | | | <_TtGC7SwiftUI17UIItemHostingViewGVS_15ModifiedContentGS1_VS_14_ViewList_ViewVVS_17CellForRowVisitor12CellModifier_VS_19DisappearedModifier__: 0x7fe6e3732330; frame = (0 0; 414 44.3333); autoresize = W+H; gestureRecognizers = <NSArray: 0x6000028a32a0>; layer = <CALayer: 0x600002675b60>> | | | | | | | | <_TtCOCV7SwiftUI11DisplayList11ViewUpdater8Platform13CGDrawingView: 0x7fe6e3628bf0; frame = (16 12; 20.6667 20.3333); anchorPoint = (0, 0); opaque = NO; autoresizesSubviews = NO; layer = <CALayer: 0x60000264fe00>> ....
_TtC7SwiftUIP33_BFB370BA5F1BADDC9D83021565761A4925UpdateCoalescingTableView
が怪しそうなので、キャストしてscrollToRowを呼んでみます。if let tableView = window.rootViewController?.view.subviews[0].subviews[0] as? UITableView { tableView.scrollToRow(at: .init(row: 10, section: 0), at: .top, animated: true) }呼べました。