- 投稿日:2021-03-22T23:15:17+09:00
この素晴らしいSwiftUIにCoreDataを!【初期設定編】
こんにちは.
Sadmachineです.
いまさらながら,このすばを見始めました.
めぐみんかわいい.この記事ではアプリ開発の基礎...
Xcodeプロジェクトの開始〜CoreDataの初期設定までをわかりやすく解説します!
スクリーンショットも適宜つけるので参考にしてください!
CoreDataのことだけ知りてえよ! っていう君はSection 2から見るといいと思うよ.LGTMたくさんしてね!!!
そもそも「CoreData」ってなに?
CoreDataとは...
Xcode上から簡単にDBのようにデータ構造を設定できたり,アプリ上で扱うデータを保存,削除,更新するプログラムを簡単に書けるようになる仕組み
参考記事:やっとわかったSwift/CoreData入門 【part1:概念編】とのこと.
もっと簡単に言うと...アプリのデータをiPhoneの端末本体内のストレージに保存して永続化するために必要なもの!
って感じでしょうか.
ざっくりすぎたかな...とりあえず,詳しいことは参考記事を見てみてね!
目次
0. 開発環境
筆者の開発環境をめっちゃ簡単に紹介します.
- Xcode 12.3
- SwitfUI
以上.
もっと詳しく知りたい人はTwitterに凸して,どうぞ.1. プロジェクトを作ろう!
以下の項目はXcodeをインストールしてある前提で話していきます.
1-1 Xcodeの起動&プロジェクトファイルを作る
Xcodeを起動すると上のような画面が出てくるので,Create a new Xcode Projectを押してください.
そうするとこんな画面が出るので[App]を選択してNext.白抜きの部分も適当な名前をつけてください.
重要なのは赤く囲まれたUse CoreDataのチェックマークです.
忘れずにつけましょう.↑MugenMemoというプロジェクト名になっていますが,これがProduct Nameです.
人によって違います.プロジェクトの作成は完了です.
1-2 ContentView.swiftの修正
1-1の最後の画像の通り,エラーが吐かれています.
とりあえず「ContentView.swift」の中身を以下のようにすればOKです.ContentView.swiftimport SwiftUI import CoreData struct ContentView: View { var body: some View { NavigationView { List { Text("TEST") } } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
ContentView.swiftの中身を変えればこんな感じでList形式でTESTという文字が出てくるはずです.
↑図ではResumeを使ってますが,エミュレーターでも同じように出ます.これでSection 1 プロジェクトを作ろうは完了です!
2. CoreDataの初期設定をしよう
CoreDataの初期設定を進めていきます.
2-1 Entityの新規追加
MugenMemo.xcdatamodeldを開いて,↑図のようにEntityを作ります.Entityを作るには,Add Entityを選択して適当な名前をつけます.
今回はMemoにしました.
Entityを作ったら,Attributeを追加します.
Attributes下の+を押して,Attribute:title Type:String にします.2-2 Entityのclass変更
ENTITESのMemoを押してDatamodelInspecter(右側の帯のいろいろな設定でるやつ)を表示させます.
↑図の赤く囲った部分Class CodegenのClass DefinitionをManual/Noneに変更します.2-3 CoreDataの管理に必要なコードを手動生成
Entity名+CoreDataClass.swift
Entity名+CoreDataProperties.swift
がないとCoreDataは使えないので手動で生成します.
ENTITESのMemoを選択した状態で,Editor/NSManagedObject Subclass...を選択.
Memo+CoreDataClass.swift
Memo+CoreDataProperties.swift
の2つができたはずです.Memo+CoreDataClass.swiftimport Foundation import CoreData @objc(Memo) public class Memo: NSManagedObject { }Memo+CoreDataProperties.swiftimport Foundation import CoreData extension Memo { @nonobjc public class func fetchRequest() -> NSFetchRequest<Memo> { return NSFetchRequest<Memo>(entityName: "Memo") } @NSManaged public var title: String? } extension Memo : Identifiable { }以上でSection 2 CoreDataの初期設定をしよう は完了です.
2-* もしもEntityにAttributeを追加したら...
Entityに別の属性のAttributeを追加した場合はEntity名+CoreDataProperties.swiftを編集します.
例えば,MemoにAttribute:text Type:Stringを追加した場合は以下のように編集しましょう.Memo+CoreDataProperties.swiftimport Foundation import CoreData extension Memo { @nonobjc public class func fetchRequest() -> NSFetchRequest<Memo> { return NSFetchRequest<Memo>(entityName: "Memo") } @NSManaged public var title: String? @NSManaged public var text: String? // ここを追加 } extension Memo : Identifiable { }「@NSManaged public var text: String?」を追加することで完了です.
まとめ
プロジェクトの開始〜CoreDataの初期設定までをわかりやすく解説しました.
長くなりそうだったので今回はここまで.次回は,
CoreDataの読み書きをしてみよう!
の予定です.
実際にSwiftUIを使いながらCoreDataに適当な文字列を追加し,保存するという一連の流れをやってみます.備忘録的なまとめですが,誰かの参考になれば嬉しいです.
少しでも参考になりましたらLGTMやフォロー,Twitterでの拡散をしていただければ幸いです.次回の記事:この素晴らしいSwiftUIにCoreDataを!【CoreData読み書き編】
参考サイト
- 投稿日:2021-03-22T22:02:28+09:00
Combine の operator を自作してみよう
Combine の登場により、Swift でようやく純正のリアクティブ1ライブラリができて、RxSwift や ReactiveSwift などの外部ライブラリに依存しなければならない時代が終わろうとしています。筆者はこれまで RxSwift をどうしても好きにはなれませんでしたが、Combine は割と好きです。心の底から「ファーストパーティ製」に対するバイアスを抜きに、個人的に Combine が一番気に入ってる部分であり、Combineの大きな特徴の一つとして、全ての operator が
protocol Publisherに準拠した独立の型になっており、operator を繋いでいくと、利用する側がこれまでどんな operator が繋がれたか、元がどんな publisher か、といったことを意識させないために、作り手にeraseToAnyPublisher()による暴露が強要しています2。これによって、これまで RxSwift のアンチパターンとも言えるよくある間違いの一つ、購読する側にそのままBehaviorRelayとして暴露してしまう問題3が仕組み上ほぼ不可能にしました。ところが、そのため逆に operator を自作するのも、RxSwift より難しいというか、そもそもどう作ればいいのかイメージが全く湧かない人も多いかと思います。この記事はそんなあなたに、operator の自作の仕方を解説したいと思います。今回は例として、2019 年の WWDC で公開した Combine in Practice に出た「API からデータを受け取って、それを自分の型にデコードして次に流す;もしデコード失敗したら代わりのデフォルト値を流す;ただしデコード失敗しても購読を終了させない」という組み合わせ技を、自作の operator にしてみます。
まずは動画のおおよそ 10 分のところを見てもらうとイメージが掴めるかと思います。API から
Dataが流されてきたら、まず普通にdecodeしてしまうと、万が一デコード失敗したら、そこで代わりのデフォルト値を流しても、購読自体はデコードエラーによって終了してしまうので、それ以降の値が一切取れなくなります。ですのでそれを避けたい場合、まずはflatMapして、その中身でJustを出してからdecodeします。そうすればデコードエラーになったとしても終了するのは最初のJustだけで済むので、flatMapによって作られたPublisherの購読は終わりません。コードにするとこんな感じです:somePublisher .flatMap({ value in Just(value) .decode(type: Output.self, decoder: decoder) .catch { _ in Just(defaultValue) } }) // .sink や .assign などでデコードした値で何かするさて、このように
flatMapを書くともちろん目的は達成しますが、やはりいちいち書くの面倒ですので、これをまとめて一つの operator にしたいですね。Combine の operator は全て
Publishersの中で定義されているので、それに合わせて同じくPublishersの中に operator の型を作りましょう。型名はRecoverableDecodeにしましょう:extension Publishers { struct RecoverableDecode { } }もちろんこれだけでは終わりませんので、ここから少しずつ肉付けしていきます。まず全ての operator は
Publisherですので、RecoverableDecodeもPublisherに適合する必要があります、そうするとassociatedtypeとしてOutputとFailureが必要になります。また、
RecoverableDecodeは必ず上流のPublisherから繋がるoperatorですので、上流の型を表現するUpstreamが必ず必要です;当然このUpstreamはPublisherです。他にこの operator は何かしらをデコードするので、
Decoderも必要です;通常の.decodeoperator として受け取るデコーダーはTopLevelDecoderに適合したものですので、それに合わせてDecoder: TopLevelDecoderにすれば OK です。そして最後に、各ジェネリクスの関係ですが、ここでデコードしかしていないのと、デコード時にエラーだけデフォルト値に置き換えているので、自分自身の
FailureはUpstream.Failureそのままです;ただし自分のOutputはデコードした型になりますので、Upstreamとは関係ないですが、その代わりOutputは必ずDecodableである必要だし、Decoder.Inputも必ずUpstream.Outputである必要があります。これで型の宣言がこうなります:struct RecoverableDecode<Upstream: Publisher, Decoder: TopLevelDecoder, Output: Decodable>: Publisher where Decoder.Input == Upstream.Output { typealias Failure = Upstream.Failure }ジェネリクスを解決したら、次は必要なパラメータです。まず oeprator である以上、必ず上流をゴニョゴニョしますので、
upstream: Upstreamはもちろん必要です;次に上流が流し込んだデータをデコードするので、decoder: Decoderも必要です;最後にデコード失敗した時のデフォルト値を返さなければいけないので、defaultValue: Outputも必要です。そしてこれらのプロパティーは外部に公開する必要が一切ないので、全てprivateで隠しておきましょう。struct RecoverableDecode<Upstream: Publisher, Decoder: TopLevelDecoder, Output: Decodable>: Publisher where Decoder.Input == Upstream.Output { // ... private let upstream: Upstream private let decoder: Decoder private let defaultValue: Output init(upstream: Upstream, decoder: Decoder, defaultValue: Output) { self.upstream = upstream self.decoder = decoder self.defaultValue = defaultValue } }さて、いよいよ肝心な operator としての機能の実装になります。
Publisherに適合する以上、func receive<S: Subscriber>(subscriber: S) where Self.Failure == S.Failure, Self.Output == S.Inputメソッドが必要になりますが、逆にいうと最後はこのメソッドだけさえ実装すれば万事解決です。ただその前に、このメソッドのジェネリクスを先に見ていきましょう。ここで
S: Subscriberのジェネリクスがありますが、これはつまり下流の購読者です。購読者はInputとして自分自身のOutputと同じ型、そしてFailureとして自分自身のFailureと同じ型である必要があるので、where Self.Failure == S.Failure, Self.Output == S.Inputの型制限がかかります。そしてその
Sである下流の購読者は、購読するときに走る処理がこのreceive処理ですので、だからこれを正しく実装すれば、繋がるとき購読処理がちゃんと動いて上流のデータが下流に流されます。ここの「正しい処理」というのは、上流のPublisherをS: Subscriberにsubscribeする、というだけのことですので、総合すると、このメソッドはこんなふうに書けば OK です:struct RecoverableDecode<Upstream: Publisher, Decoder: TopLevelDecoder, Output: Decodable>: Publisher where Decoder.Input == Upstream.Output { // ... func receive<S: Subscriber>(subscriber: S) where Self.Failure == S.Failure, Self.Output == S.Input { upstream .flatMap({ value in Just(value) .decode(type: Output.self, decoder: decoder) .catch { _ in Just(defaultValue) } }) .subscribe(subscriber) } }
flatMapの中身は上で紹介したコードと全く一緒で、最後に.subscribe(subscriber)4 を繋ぐだけで、購読処理が実装されます。ね?意外と難しくないでしょ?ただしもちろんこれだけではメソッドチェーンには使えないので、最後は
Publisherに使えるメソッドチェーン用のメソッドを作れば終了です:extension Publisher { func recoverableDecode<Output: Decodable, Decoder: TopLevelDecoder>(_ output: Output.Type, from decoder: Decoder, onErrorJustReturn defaultValue: Output) -> Publishers.RecoverableDecode<Self, Decoder, Output> where Decoder.Input == Self.Output { return Publishers.RecoverableDecode(upstream: self, decoder: decoder, defaultValue: defaultValue) } }こうすれば、今度このようなシチュエーションは面倒な
flatMapを書かなくても、recoverableDecodeひとつで面倒なことが全て解決されます;たとえば Playground ならこんな簡単な確認ができます:var cancellables: Set<AnyCancellable> = [] let datum: [Data] = ["1", nil, "3"] .map({ $0.map { try! JSONEncoder().encode($0) } ?? Data() }) datum.publisher .recoverableDecode(String.self, from: JSONDecoder(), onErrorJustReturn: "Decode Error") .sink(receiveValue: { print($0) }) .store(in: &cancellables) // ["1", "Decode Error", "3"]operator の作り方を理解できれば、独自の処理を挟みたい時は簡単にそのための operator が作れるようになりますので、ぜひ色々作ってみましょう
![]()
厳密には Combine では Hot/Cold の概念がなかったり、Stream の意識が薄かったりで、一般的なよくあるリアクティブライブラリとは異なる部分も多いので、筆者はあまり Combine を「リアクティブ」と呼ぶのはあまり好きではありませんが、間違いなくリアクティブにインスパイアされたはずだし、共通するコンセプトもかなり多いので、イメージが掴みやすいため一応本記事でも「リアクティブ」と呼びます。 ↩
たとえば
Just<Int>の publisher の後ろにmapという operator を繋げると、その結果がMap<Just<Int>, Output>になるので、そのままJust<Int>で公開すると型が一致しないためビルドエラーになるので、AnyPublisher<Output, Never>で公開し、operator を繋いだら最後にeraseToAnyPublisher()を呼び出して型を落とし込むことが実質強要されています。 ↩元の
BehaviorRelayなどの relay や subject を購読する側に暴露してしまうと、仕組み上購読する側も新しいイベントを流せてしまうので、本来であればそれらはObservableに落とし込んで公開するのが一番望ましいですが、まあ微妙にちょっと一手間かかるし、最低限のリアクティブの知識さえあればそんな汚いコード書かないはずですので、そのままBehaviorRelayとかで公開してしまうコードもよく見掛けられます。 ↩ただしこれ英語の文法としては実は微妙に違くて、
subscribeは「購読する」の意味なので、この文法ですと「購読者を購読する」という逆の意味になってしまいます。もちろんPublisherからSubscriberへの流れの方がメソッドチェーンが組みやすいのでこうなってしまうのは気持ちはわかりますが、せめてgetSubscribed(by: subscriber)とかの API にして欲しかったりします。 ↩
- 投稿日:2021-03-22T19:36:06+09:00
SwiftでiOSアプリを作ってみた
SwiftでiOSアプリを作ってみた
コーダーズ インターン生のNです。
本記事では、私自身がSwiftの学習を行った経緯や、実際にアプリを作成した感想までを記載しています。プログラミング学習を始めたきっかけ
ここでは、私が独学でプログラミング学習を始めた経緯について書いていきます。私は大学で情報系の学科に所属しています。大学では、基本的なプログラミングや情報技術について学んできました。
ここで、情報系の学部にいて気づいたことがあります。それは、プログラミング力が身につくかどうかは人による、という事です。
大学の教える先生にもよると思いますが、私の受けた授業では、プログラミング言語の基本的なルールや、一般的なコードの書き方を教えてもらえるだけでした。このことから私は、文系出身と情報系出身とではエンジニアとしての能力に大きな差は無いと考えています。実際にIT企業の会社説明会などに行くと、人事の方が「入社して数ヶ月で文系出身と理系出身のエンジニアの差はなくなる。」と言っているのをよく聞きます。
そこで私は、社会に出た時に自信を持って「情報系の学部出身です。」と言えるようになりたいと思い、独学によるプログラミング学習を始めました。
学習するプログラミング言語の決め方
私が独学で学習を行った言語は、Swiftです。聞き馴染みがない方も多いかもしれませんが、SwiftとはiPhoneやiPadなどの端末で使えるアプリ、もしくはMac向けのアプリを開発することができる言語です。私がSwiftを学ぼうと思った理由は、アプリという成果物を目に見える形で得られるからです。
私自身、あまり我慢強いタイプではないため、これまでもプログラミング学習が続かなかったという経験があります。しかし、このように勉強を頑張った証を形で残せるというのは、モチベーションの維持に大きく役立つと考えました。さらに、Swiftで作成したアプリは、Xcodeという開発環境を利用することで自分のiPhoneで動かすことができるのです。自分で作成した自分だけのアプリを使う状況を想像して、ワクワクしました。これは後から知った情報ですが、2022年から高校で必修科目になる「情報I」は、JavaScript、Python、VBA、Swiftが含まれており、全員が必ず学ぶ言語になるとのことです。このことから、Swiftは今後も利用されることが多くなると言え、今学んでおくべき言語の一つであると言えます。
学習方法
ここでは、私がどのような計画で学習を進めたのかについて書いていきます。私は、勉強開始当初だけはモチベーションが高いのですが、ある時を境にやる気がなくなり、だんだんと勉強から遠のいてしまったという経験が多々あります。私はこの怠惰な自分を矯正するためにある工夫を行いました。
それは、毎週○曜日の×時〜×時と決めた時間だけを勉強に充てるということです。ここでのポイントは“だけ”というところです。
勉強開始当初はモチベーションが高く、1日に何時間でも学習できることもあるかと思います。しかしこれでは、短距離走のように少し走っただけで疲れてしまいます。長期的に計画を立て、自分にあったペースで習慣的に学習を行うことがモチベーションの維持に有効だと私は考えました。さらにこの方法は、学習した内容をしっかりと身に着けるという面でも有効だと思います。数日かけて一気に勉強するという方法では、脳に学習内容が定着しません。長期的かつ習慣的に学習を進めることで、勉強した内容を確実に自分のものにすることができるのです。具体的に私が設定したルールは、火曜日と木曜日の11:00-12:00は必ず勉強するというものです。勉強開始当初は、こんな短い時間でも集中力が維持できず、PCに向き合うことに辛さを感じました。しかし、ルールを守れたという達成感を積み重ねていくと、だんだんと学習が辛いという気が無くなって来て、習慣的に学習を進めることができるようになりました。設定した勉強時間に慣れてきたら、徐々に一日の学習時間を伸ばしたり、日数を増やすことで学習を計画的に進めていきました。
学習準備
学習を始めるにあたり、まずは参考書を選ぶところから始めました。私は文字ばかりの本だとすぐに疲れてしまうので、できるだけ図や表の多いものを探しました。そして見つけたのが、「絶対に挫折しないiPhoneアプリ開発「超」入門」という本です。タイトル通り、初心者に寄り添った、かなり易しい内容になっています。内容としては、開発環境の準備の仕方から基本的なコードの書き方、そして実際にアプリを世に出す方法までが丁寧に分かりやすく書かれています。
この内容なら続けられるだろうと感じ、購入を決めました。そして実際に学習を進めた結果、特に行き詰まってしまう箇所はなかったように思います。初心者の方もiOSアプリを作成してみたい方にはオススメの本です。Swiftを勉強してみた感想
さて、ここからは実際にSwiftを勉強してみた感想について書いていきます。
まず初めにお伝えしたいのが、Swiftは他の言語と基本的な構造は同じだということです。私はこれまでC言語やJavaなどの言語を学習してきましたが、Swiftにはこれらと共通した作りが多く登場しました。細かい記述方法などはもちろん異なる部分がありますが、変数の扱い方や配列の考え方、forループ文の書き方など似ている部分が多くプログラミング経験者はアドバンテージが活かせると思います。また、学習を進めていけばいくほど面白く感じられるという特徴があると思います。
初めに学ぶ基本的なコード記述方法などは、他のプログラミング言語と同じように、若干我慢が必要な内容と言えます。しかしこれを乗り越えると、アプリに出てくる部品の扱い方を学ぶことになります。この内容が私にはとても面白く感じました。具体的には、普段iPhoneを使っていて目にしているボタンの扱い方を学べることなどです。これらをUI部品と呼びますが、アプリ制作に近づいているという感覚が得られてワクワクするような内容でした。実際にアプリ制作を行ってみた感想
ここでは、私が実際に作成したアプリについてご紹介します。
私が作成したアプリは「じゃんけんアプリ」です。紹介するのも恥ずかしいくらいのチープな設計ですが、初心者ながら頑張って制作しました。制作期間は約二週間から三週間くらいでした。
機能としてはまずアプリを開き、ジャンケンをするかしないかを選択します。そして“はい”を選択すると、ランダムで表示される相手の手に対してじゃんけんの勝負を行います。そして、最終的に勝負の結果を出力するというというものです。以下がその開発画面になります。アプリ制作を実際に行ってみた感想として、想像していたよりも大変だったと感じました。先ほど紹介した簡単なじゃんけんアプリにおいても、一見しただけでは見えてこない機能が多く含まれています。例えば、ボタンを押して何秒後に画面遷移を行うかの設定だったり、相手の手を決定するための乱数の発生機能、そしてじゃんけんの勝ち負けを判定する機能、じゃんけんの結果をもとに画面の遷移先を決定するための機能などです。一つの動作ごとに実装しなければいけないことがたくさんあるので、かなりの時間を要しました。
また、エラーへの対処が大変だったと感じました。Xcodeにおいて実行を行う際、エラーの位置やその原因を特定するのが慣れていないと少し分かりにくいように思えました。そのような時は、エラー文を元に検索を行いteratailやqiitaなどに書かれた解決策をもとに対処しました。ネット上にアプリ開発のブログ等がとても多く、アプリ制作を行う上でとても参考になりました。
また、楽しかった点として、やはり形になっていく過程は面白いと思いました。Xcodeでは、実際にiPhoneやMacなどでのアプリの挙動を確認できる「Simulator」という機能があります。以下にSimulatorの画面の一例を載せています。
実際にアプリを動かしながら作成できるというこの機能は、かなりモチベーションの維持に役立ちました。
学習をしてよかったこと
ここでは、Swiftを学習してよかったことについて書いていきます。
まず一つ目は、アプリの構造を学べたということです。先ほども述べましたが、普段アプリを利用しているだけでは気づけないような仕組みについて知ることができました。次に、就活などで話せる話題になるということです。ただプログラミング学習を行った、だとか、どれくらいの期間勉強した、という言葉では上手く実力をアピールすることは出来ません。しかし、アプリを開発したということになると、それだけで興味を持ってもらえます。実際に私も、面接官の方にアプリ制作の経験を伝えたことで興味を持っていただき、「どんなアプリを作ったの?」だったり、「作った上で大変だったことは?」などと多くのことを質問してもらうことができました。これは、かなりのアピールに繋がったのではないかと思っています。このように、他人に伝わりやすい成果を持っているというのは、就活などにおいて一つの強みになると感じました。
今後について
今後は、さらに複雑なアプリ開発を進めていきたいと考えています。そして複雑なアプリを作れるようになったら、いずれはアプリを世に出したいと思っています。人気のアプリを作るにはまだまだ多くのことを学ぶ必要があると思いますが、これからも楽しんで学習を進めていきたいと思っています。
読んでいただきありがとうございました。
- 投稿日:2021-03-22T18:32:59+09:00
Model データ周り 自分用
Swift//クイズのデータ struct Quiz: Decodable { //クイズのID let quizID: Int? //クイズの問題文 let question: String //クイズの選択肢 let choices: [String]? //クイズの答え let answer: String //クイズの難易度 let difficulty: Int //クイズの解説 let commentary: String? //4択かどうか var isFourChoices: Bool { return choices != nil } }Kotlindata class Quiz ( /** クイズのID */ val quizID: Int?, /** クイズの問題文 */ val question: String, /** クイズの選択肢 */ val choices: List<String>?, /** クイズの答え */ val answer: String, /** クイズの難易度 */ val difficulty: Int, /** クイズのコメント */ val commentary: String? ) { /** 4択かマルバツかの分岐 */ val isFourChoices: Boolean get() = choices != null }
- 投稿日:2021-03-22T12:22:46+09:00
[swift×Firebase]ドキュメントIDから単体情報をクエリする方法
やりたかったこと
ドキュメントIDを使用して、各データにアクセスしたかった。
今回は、複数ではなく単体のデータさえ返って来ればよかった。やったこと
以下のようにドキュメントIDはクエリ条件に設定することが可能。
「.whereField(FieldPath.documentID(), in: [documentid])」
func getMemo(documentid:String){ let db = Firestore.firestore() db.collection("memoes") .whereField(FieldPath.documentID(), in: [documentid]) .getDocuments(){ (querySnapshot, err) in if let error = err{ print("error: \(error.localizedDescription)") return } guard let snapshot = querySnapshot else{ return } for document in snapshot.documents{ print("ドキュメントタイトル:\(document.get("title") as! String)") } } }これでできた。
- 投稿日:2021-03-22T10:49:54+09:00
画像の色を抽出し、背景色にセットしてみた
はじめに
自然界のものには、それに対応する色があります。(バナナなら黄色、りんごなら赤、のような)
(光が当たって反射してくる〜のような、物理的な話はおいておきます)また、デザインされたものに関しては複数の色が使われることが多いかと思います。
というのも、デザインの基本的な考え方として、こちらに記されるような配色の考え方が存在します。一般的に、基本カラー3色を「70%:25%:5%」の比率にして配色すると、バランスの取れた美しい配色になるとされています。
最も大きな面積を占める色を「ベースカラー(70%)」、ブランドのイメージカラーなどデザインの中心になる色を「メインカラー(25%)」、画面にアクセントを持たせるための色を「アクセントカラー(5%)」と呼びます。つまり、マンガアプリのサムネイルやキャラクターデザイン等では、色によって与えたい印象やイメージ、そのものの特性等を表すことはよくあります。
その色をUIに反映させたいな、と思ったのが今回のこの記事です。成果物
実装
今回実装した
- 一番使われている色を抽出(スクショ左側)
- 使われている色の平均色を計算(スクショ右側)
それぞれについて見ていきます。
共通
色情報を取得する部分に関しては、一旦同じ処理としています。
(本当はそれぞれで適した処理をするのが速度的に良いと思う)懸念点としては、イラスト等で背景色を敷いているもので、白や黒等の背景色が一番使われている色だった場合、ですね。
イラストと全然関係ない色が抽出されてしまいます。
その対応として今回は、除外する色を指定しておくことにしました。
プロジェクトごとに扱う色のフォーマットが決まっていれば、その色を除外することで有効色を抽出できそうです。ColorFrequency.swiftstruct ColorFrequency { let color: ColorFactor var count: Int }ColorFactor.swiftstruct ColorFactor: Equatable { var red: Int var green: Int var blue: Int var alpha: Int static var zero: ColorFactor { return .init(red: 0, green: 0, blue: 0, alpha: 0) } var uiColor: UIColor { return .init( red: CGFloat(self.red) / 255.0, green: CGFloat(self.green) / 255.0, blue: CGFloat(self.blue) / 255.0, alpha: CGFloat(self.alpha) / 255.0 ) } func calculateMixedColor(count: Int) -> UIColor { return .init( red: CGFloat(self.red) / CGFloat(count * 255), green: CGFloat(self.green) / CGFloat(count * 255), blue: CGFloat(self.blue) / CGFloat(count * 255), alpha: CGFloat(self.alpha) / CGFloat(count * 255) ) } }PickedImageColorView.swiftfinal class PickedImageColorView: BaseView { @IBOutlet private weak var innerView: UIView! @IBOutlet private weak var imageView: UIImageView! private var colorFrequencies: [ColorFrequency] = [] // 無視したい色 private let excludeColors: [ColorFactor] = [ .init(red: 0, green: 0, blue: 0, alpha: 0), .init(red: 1, green: 1, blue: 1, alpha: 1), ] func setImage(_ imageUrl: String, pickType: PickType) { self.imageView.setImage(with: imageUrl, placeholder: nil, completed: { [weak self] response in guard let self = self else { return } guard case .success(let imageResponse) = response else { return } self.pickColor(image: imageResponse.image) let pickedColor: UIColor = // ここで色を選定 self.innerView.backgroundColor = pickedColor }) } } // MARK: - pick color extension PickedImageColorView { private func pickColor(image: UIImage) { guard let provider = image.cgImage?.dataProvider, let data = CFDataGetBytePtr(provider.data) else { return } let numberOfComponents = 4 let maxWidth: Int = Int(image.size.width) let maxHeight: Int = Int(image.size.height) for x in 0..<maxWidth { for y in 0..<maxHeight { let targetPixel = (maxWidth * y + x) * numberOfComponents let color: ColorFactor = .init( red: Int(data[targetPixel]), green: Int(data[targetPixel + 1]), blue: Int(data[targetPixel + 2]), alpha: Int(data[targetPixel + 3]) ) if !self.excludeColors.contains(color) { self.arrangeColorFrequency(color: color) } } } } private func arrangeColorFrequency(color: ColorFactor) { if let index = self.colorFrequencies.firstIndex(where: { $0.color == color }) { self.colorFrequencies[index].count += 1 } else { self.colorFrequencies.append(.init(color: color, count: 1)) } } }一番使われている色を抽出
画像の中で一番使われている色、上の説明でいくとベースカラー。
一番使われている色、なので、各ピクセルの色を抽出し、その回数さえわかれば選定できそうです。コード的にはこう。
// MARK: - pick color extension PickedImageColorView { private func getMajorColor(colorFrequencies: [ColorFrequency]) -> UIColor { guard let majorColor: UIColor = colorFrequencies.sorted(by: { $0.count > $1.count }).first?.color.uiColor else { return .init() } return majorColor } }使われている色の平均色を計算
平均色なので、各ピクセルの色とそれが使われた回数がわかったら、対象ピクセル数で平均化できそうです。
こちらも先ほど同様、除外する色を指定して、それ以外を対象ピクセルとすることで有効色を抽出します。
コード的にはこう。
// MARK: - pick color extension PickedImageColorView { private func getMixedColor(colorFrequencies: [ColorFrequency]) -> UIColor { var color: ColorFactor = .zero let count: Int = colorFrequencies.reduce(0) { $0 + $1.count } colorFrequencies.forEach { colorFrequency in let singleCount: Int = colorFrequency.count color.red += colorFrequency.color.red * singleCount color.green += colorFrequency.color.green * singleCount color.blue += colorFrequency.color.blue * singleCount color.alpha += colorFrequency.color.alpha * singleCount } return color.calculateMixedColor(count: count) } }それぞれの使い方
あとは、それぞれどう使うかってところなんですが、まぁenumで管理しておけばいいかなと。
PickType.swiftenum PickType { case major // 一番使われている色 case mixed // 平均色 }こういうのを用意しておけば、共通のコードのところが以下のようになります。
PickedImageColorView.swiftfinal class PickedImageColorView: BaseView { func setImage(_ imageUrl: String, pickType: PickType) { self.imageView.setImage(with: imageUrl, placeholder: nil, completed: { [weak self] response in guard let self = self else { return } guard case .success(let imageResponse) = response else { return } self.pickColor(image: imageResponse.image) let pickedColor: UIColor = { switch pickType { case .major: return self.getMajorColor(colorFrequencies: self.colorFrequencies) case .mixed: return self.getMixedColor(colorFrequencies: self.colorFrequencies) } }() self.innerView.backgroundColor = pickedColor }) } }画像URLとどの方法で色を選定するか、を指定して呼び出して、どうぞ。
最後に
今回はほんの誤差でもあれば「違う色」としましたが、実際使用するときは、ある程度の誤差を許容する方が良い場合もあるので、必要に応じてやってみてもいいかもです!
実装して、できたーと思ったらこんな記事を発見しまして。
https://qiita.com/shu223/items/805a179cfe83c47ec0f9
なるほど・・・という感じなんですが、想像した機能を自分で実現する術を知るというのは大事だと思うので、それをやってみました的な記事だと受け取ってもらえればいいかなと。
最終的にライブラリの方が良いな、とか、時間の都合で自分で試行錯誤してる場合じゃないというのは往々にしてあるとは思いますが、
こういう機能を作りたいとか、こういうことを実現したい、ってなったときに、ライブラリを探すのではなく、一旦自分でこうやったらいけるかな?と考えたりプロトタイプ実装してみたりしていくと自身の力になる説。
一旦が大事説。
- 投稿日:2021-03-22T09:11:12+09:00
【SwiftUI】背景をタップしてキーボードを閉じる
- 投稿日:2021-03-22T01:10:36+09:00
UISceneDelegateをつかってUniversal Linkからの起動を制御する
概要
iOS13以降から
UISceneDelegateが導入されたのでUniversal Linkからアプリが起動された挙動もUISceneDelegateのライフサイクルで制御します。
遷移したURLによってiOS側で制御を行いたいと思いますので、そちらを試した結果を記します。実装
Universal Linkを利用するための
apple-app-site-associationの設置やAssociated Domainsの設定は完了しているものとします。
XCode11以降で作成されたプロジェクトにはSceneDelegate.swiftが含まれていてこちらにてライフサイクルの制御が管理されています。Universal LinkなどをつかってSafariなどから遷移してきたときは
scene(_:continue:)というDelegateメソッドで補足されます。
https://developer.apple.com/documentation/uikit/uiscenedelegate/3238056-scene
NSUserActivityにwebpageURLというプロパティが存在するのでこちらでURLを取得しURLによって表示する画面などを決めて遷移できます。以下は
hogehogeというパスのURLでアプリに遷移した際にHogeHogeViewControllerをモーダル表示するときの実装です。SceneDelegate.swiftfunc scene(_ scene: UIScene, continue userActivity: NSUserActivity) { guard userActivity.activityType == NSUserActivityTypeBrowsingWeb else { return } guard let url = userActivity.webpageURL else { return } if url.scheme == "https" && url.host == "example.jp" && url.path == "hogehoge" { window?.rootViewController?.present(HogeHogeViewController(), animated: true, completion: nil) } }問題
上記の実装でUniversal Linkから遷移を制御できるのですが、こちらはアプリがバックグラウンドで起動中しか保続できません。
新規にアプリが起動するときを制御するにはscene(_:willConnectTo:options:)にて同様の処理を用意する必要があります。SceneDelegate.swiftfunc scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { // URLが存在するものを補足 guard let userActivity = connectionOptions.userActivities.first(where: { $0.webpageURL != nil }) else { return } guard userActivity.activityType == NSUserActivityTypeBrowsingWeb else { return } if url.scheme == "https" && url.host == "example.jp" && url.path == "hogehoge" { window?.rootViewController?.present(HogeHogeViewController(), animated: true, completion: nil) } }こちらでアプリが起動していない場合でも画面の制御を行うことができるようになります。
あとがき
scene(_:continue:)だけだと未起動時の制御を補足できないですが、起動していないので実装時に見落としてしまいがちなのとデバッグがしにくく、ハマってしまうこともあるかとも思います。
こちらが役に立てば。











